diff --git a/Jenkinsfile b/Jenkinsfile index eee82b2..3a15cb6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -149,7 +149,7 @@ pipeline { def nodeJS = tool 'NodeJS11'; withSonarQubeEnv('Sonarqube') { sh '''sed -i "s|/plone/instance/src/$GIT_NAME|$(pwd)|g" coverage.xml''' - sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.python.xunit.skipDetails=true -Dsonar.python.xunit.reportPath=xunit-reports/*.xml -Dsonar.python.coverage.reportPath=coverage.xml -Dsonar.sources=./eea -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER" + sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.python.xunit.skipDetails=true -Dsonar.python.xunit.reportPath=xunit-reports/*.xml -Dsonar.python.coverage.reportPaths=coverage.xml -Dsonar.sources=./eea -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER" sh '''try=2; while [ \$try -gt 0 ]; do curl -s -XPOST -u "${SONAR_AUTH_TOKEN}:" "${SONAR_HOST_URL}api/project_tags/set?project=${GIT_NAME}-${BRANCH_NAME}&tags=${SONARQUBE_TAGS},${BRANCH_NAME}" > set_tags_result; if [ \$(grep -ic error set_tags_result ) -eq 0 ]; then try=0; else cat set_tags_result; echo "... Will retry"; sleep 60; try=\$(( \$try - 1 )); fi; done''' } } diff --git a/docs/HISTORY.txt b/docs/HISTORY.txt index f70a70b..de9b3fa 100644 --- a/docs/HISTORY.txt +++ b/docs/HISTORY.txt @@ -1,6 +1,15 @@ Changelog ========= +13.0 - (2021-06-16) +--------------------------- +* Feature: Added GET RestAPI endpoint for Daviz Charts @charts + [avoinea refs #126277] +* Feature: Added GET RestAPI endpoint for Data Table @table + [avoinea refs #133973] +* Feature: Added GET RestAPI endpoint for IDataProvenance @provenances + [iulianpetchesi refs #123935] + 12.7 - (2020-12-16) --------------------------- * Change: load google charts version 49 in order to avoid warning diff --git a/eea/app/visualization/common.zcml b/eea/app/visualization/common.zcml index 7f9b19c..fb3c7ca 100644 --- a/eea/app/visualization/common.zcml +++ b/eea/app/visualization/common.zcml @@ -11,6 +11,7 @@ + diff --git a/eea/app/visualization/restapi/__init__.py b/eea/app/visualization/restapi/__init__.py new file mode 100644 index 0000000..637fa62 --- /dev/null +++ b/eea/app/visualization/restapi/__init__.py @@ -0,0 +1,2 @@ +""" RestAPI +""" \ No newline at end of file diff --git a/eea/app/visualization/restapi/charts/__init__.py b/eea/app/visualization/restapi/charts/__init__.py new file mode 100644 index 0000000..637fa62 --- /dev/null +++ b/eea/app/visualization/restapi/charts/__init__.py @@ -0,0 +1,2 @@ +""" RestAPI +""" \ No newline at end of file diff --git a/eea/app/visualization/restapi/charts/configure.zcml b/eea/app/visualization/restapi/charts/configure.zcml new file mode 100644 index 0000000..b917911 --- /dev/null +++ b/eea/app/visualization/restapi/charts/configure.zcml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/eea/app/visualization/restapi/charts/get.py b/eea/app/visualization/restapi/charts/get.py new file mode 100644 index 0000000..4393e96 --- /dev/null +++ b/eea/app/visualization/restapi/charts/get.py @@ -0,0 +1,52 @@ +""" Charts +""" +from plone.restapi.interfaces import IExpandableElement +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.services import Service +from Products.CMFPlone.interfaces import IPloneSiteRoot +from zope.component import adapter, queryMultiAdapter +from zope.interface import implementer +from zope.interface import Interface +from eea.app.visualization.interfaces import IVisualizationEnabled + +@implementer(IExpandableElement) +@adapter(IVisualizationEnabled, Interface) +class Charts(object): + """ Charts + """ + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + result = {"charts": {"@id": "{}/@charts".format( + self.context.absolute_url())}} + + if not expand: + return result + + if IPloneSiteRoot.providedBy(self.context): + return result + + view = queryMultiAdapter(( + self.context, self.request), + name='daviz-view.html' + ) + + if not view: + return result + + result["charts"]["items"] = [] + for tab in view.tabs: + result["charts"]["items"].append(json_compatible(tab)) + return result + + +class ChartsGet(Service): + """Get charts information""" + + def reply(self): + """ Reply + """ + info = Charts(self.context, self.request) + return info(expand=True)["charts"] diff --git a/eea/app/visualization/restapi/configure.zcml b/eea/app/visualization/restapi/configure.zcml new file mode 100644 index 0000000..88dcf3f --- /dev/null +++ b/eea/app/visualization/restapi/configure.zcml @@ -0,0 +1,5 @@ + + + + + diff --git a/eea/app/visualization/restapi/data/__init__.py b/eea/app/visualization/restapi/data/__init__.py new file mode 100644 index 0000000..637fa62 --- /dev/null +++ b/eea/app/visualization/restapi/data/__init__.py @@ -0,0 +1,2 @@ +""" RestAPI +""" \ No newline at end of file diff --git a/eea/app/visualization/restapi/data/configure.zcml b/eea/app/visualization/restapi/data/configure.zcml new file mode 100644 index 0000000..4418193 --- /dev/null +++ b/eea/app/visualization/restapi/data/configure.zcml @@ -0,0 +1,4 @@ + + + + diff --git a/eea/app/visualization/restapi/data/provenance/__init__.py b/eea/app/visualization/restapi/data/provenance/__init__.py new file mode 100644 index 0000000..637fa62 --- /dev/null +++ b/eea/app/visualization/restapi/data/provenance/__init__.py @@ -0,0 +1,2 @@ +""" RestAPI +""" \ No newline at end of file diff --git a/eea/app/visualization/restapi/data/provenance/configure.zcml b/eea/app/visualization/restapi/data/provenance/configure.zcml new file mode 100644 index 0000000..3742c49 --- /dev/null +++ b/eea/app/visualization/restapi/data/provenance/configure.zcml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/eea/app/visualization/restapi/data/provenance/get.py b/eea/app/visualization/restapi/data/provenance/get.py new file mode 100644 index 0000000..c785747 --- /dev/null +++ b/eea/app/visualization/restapi/data/provenance/get.py @@ -0,0 +1,68 @@ +""" RestAPI GET enpoints +""" +from zope.publisher.interfaces import IPublishTraverse +from zope.interface import implementer +from zope.interface import Interface +from zope.component import adapter, queryAdapter +from plone.restapi.services import Service +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.interfaces import IExpandableElement +from eea.app.visualization.interfaces import IDataProvenance +from eea.app.visualization.interfaces import IMultiDataProvenance +from eea.app.visualization.interfaces import IVisualizationEnabled +from Products.CMFPlone.interfaces import IPloneSiteRoot + + +@implementer(IExpandableElement) +@adapter(IVisualizationEnabled, Interface) +class DataProvenance(object): + """ Get data provenances + """ + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + result = {"provenances": { + "@id": "{}/@provenances".format(self.context.absolute_url()), + }} + + if not expand: + return result + + if IPloneSiteRoot.providedBy(self.context): + return result + + result['provenances']['items'] = [] + + # Get IMultiDataProvenance + multi = queryAdapter(self.context, IMultiDataProvenance) + if multi: + provenances = json_compatible(multi.provenances) + result['provenances']['items'].extend(provenances) + + source = queryAdapter(self.context, IDataProvenance) + if (getattr(source, 'link', None) and + getattr(source, 'title', None) and + getattr(source, 'owner', None)): + provenance = { + "title": json_compatible(source.title), + "owner": json_compatible(source.owner), + "link": json_compatible(source.link) + } + + if getattr(source, "copyrights", None): + provenance['copyrights'] = json_compatible(source.copyrights) + + result['provenances']['items'].append(provenance) + return result + + +@implementer(IPublishTraverse) +class Get(Service): + """GET""" + + def reply(self): + """Reply""" + info = DataProvenance(self.context, self.request) + return info(expand=True)["provenances"] diff --git a/eea/app/visualization/restapi/data/provenance/post.py b/eea/app/visualization/restapi/data/provenance/post.py new file mode 100644 index 0000000..8a0e24e --- /dev/null +++ b/eea/app/visualization/restapi/data/provenance/post.py @@ -0,0 +1,126 @@ +""" RestAPI enpoint POST +""" +from zope.publisher.interfaces import IPublishTraverse +from zope.interface import implementer, alsoProvides +from zope.interface import Interface +from zope.component import adapter, queryAdapter +from plone.restapi.services import Service +from plone.restapi.interfaces import IExpandableElement +from plone.restapi.deserializer import json_body +from eea.app.visualization.interfaces import IDataProvenance, IMultiDataProvenance +from Products.CMFPlone.interfaces import IPloneSiteRoot + + +import plone.protect.interfaces + + +@implementer(IExpandableElement) +@adapter(Interface, Interface) +class DataProvenance(object): + """ Set DataProvenance + """ + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, data=[]): + if IPloneSiteRoot.providedBy(self.context): + self.request.response.setStatus(400) + return dict( + error=dict( + type="BadRequest", + message="Tried to set data provenances on site root.", + ) + ) + + source = queryAdapter(self.context, IDataProvenance) + multi = queryAdapter(self.context, IMultiDataProvenance) + if not source and not multi: + self.request.response.setStatus(400) + return dict( + error=dict( + type="BadRequest", + message="Can't adapt IDataProvenance/IMultiDataProvenance to context.", + ) + ) + + multi_provenances = [] + provenances = data.get('provenances', []) + if len(provenances) < 1: + self.request.response.setStatus(400) + return dict( + error=dict( + type="BadRequest", + message="No data provenances provided.", + ) + ) + + # differentiate between multi and normal provenances + if not len(provenances) == 1: + if not multi: + multi_provenances = [] + if not source: + multi_provenances = provenances + provenances = [] + if multi and source: + multi_provenances = [prov for prov in provenances if prov.get('multi', True) != 'False'] + provenances = [prov for prov in provenances if prov not in multi_provenances] + + # if more than one non multi provenance is given, save the last one + if len(provenances) > 1: + provenances = [provenances[-1]] + + # True if len == 1 + if len(provenances) == 1: + data = provenances[0] + source.title = data["title"] + source.owner = data["owner"] + source.link = data["link"] + + if "copyrights" in data and hasattr(source, "copyrights"): + copyrights = data['copyrights'] + + if len(copyrights) > 2 and isinstance(copyrights, list): + self.request.response.setStatus(400) + return dict( + error=dict( + type="BadRequest", + message="Copyrights must be a list with <= 2 items or string.", + ) + ) + + if isinstance(copyrights, (str, unicode)): + source.copyrights = copyrights + else: + source.copyrights = tuple(copyrights) + elif "copyrights" in data: + self.request.response.setStatus(400) + return dict( + error=dict( + type="BadRequest", + message="Can't set copyrights, not a blob object.", + ) + ) + + # multi provenances + if len(multi_provenances) > 0: + multi.provenances = multi_provenances + + self.request.response.setStatus(200) + return dict(message="Successfully set data provenance") + + +@implementer(IPublishTraverse) +class Post(Service): + """POST""" + + def reply(self): + """Reply""" + data = json_body(self.request) + + # Disable CSRF protection + if "IDisableCSRFProtection" in dir(plone.protect.interfaces): + alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) + + post = DataProvenance(self.context, self.request) + return post(data) \ No newline at end of file diff --git a/eea/app/visualization/restapi/data/table/__init__.py b/eea/app/visualization/restapi/data/table/__init__.py new file mode 100644 index 0000000..637fa62 --- /dev/null +++ b/eea/app/visualization/restapi/data/table/__init__.py @@ -0,0 +1,2 @@ +""" RestAPI +""" \ No newline at end of file diff --git a/eea/app/visualization/restapi/data/table/configure.zcml b/eea/app/visualization/restapi/data/table/configure.zcml new file mode 100644 index 0000000..b88da2b --- /dev/null +++ b/eea/app/visualization/restapi/data/table/configure.zcml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/eea/app/visualization/restapi/data/table/get.py b/eea/app/visualization/restapi/data/table/get.py new file mode 100644 index 0000000..d420a06 --- /dev/null +++ b/eea/app/visualization/restapi/data/table/get.py @@ -0,0 +1,53 @@ +""" RestAPI GET enpoints +""" +from zope.publisher.interfaces import IPublishTraverse +from zope.interface import implementer +from zope.interface import Interface +from zope.component import adapter, queryMultiAdapter +from plone.restapi.services import Service +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.interfaces import IExpandableElement +from Products.CMFPlone.interfaces import IPloneSiteRoot +from eea.app.visualization.interfaces import IVisualizationEnabled + + +@implementer(IExpandableElement) +@adapter(IVisualizationEnabled, Interface) +class DataTable(object): + """ Get data table + """ + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, expand=False): + result = {"table": { + "@id": "{}/@table".format(self.context.absolute_url()), + }} + + if not expand: + return result + + if IPloneSiteRoot.providedBy(self.context): + return result + + view = queryMultiAdapter(( + self.context, self.request), + name='download.table' + ) + + if not view: + return result + + result['table'].update(json_compatible(view.data)) + return result + + +@implementer(IPublishTraverse) +class Get(Service): + """GET""" + + def reply(self): + """Reply""" + info = DataTable(self.context, self.request) + return info(expand=True)["table"] diff --git a/eea/app/visualization/version.txt b/eea/app/visualization/version.txt index 6754387..f075061 100644 --- a/eea/app/visualization/version.txt +++ b/eea/app/visualization/version.txt @@ -1 +1 @@ -12.7 +13.0