diff --git a/docs/changes.rst b/docs/changes.rst index 82817a2a..0f228dfa 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,12 @@ Changelog 11.0.0 (unreleased) ------------------- +- Add ``@navtree`` REST API endpoint. + [maurits] + +- Add ``@tool-versions`` REST API endpoint. + [maurits] + - Prevent the choice widget to throw the error: ``TypeError: object of type 'CatalogSource' has no len()`` [ale-rt] diff --git a/src/osha/oira/configure.zcml b/src/osha/oira/configure.zcml index 3fc15937..2af2dcbd 100644 --- a/src/osha/oira/configure.zcml +++ b/src/osha/oira/configure.zcml @@ -35,6 +35,7 @@ + + + + + + + diff --git a/src/osha/oira/services/navigation.py b/src/osha/oira/services/navigation.py new file mode 100644 index 00000000..58656b5f --- /dev/null +++ b/src/osha/oira/services/navigation.py @@ -0,0 +1,67 @@ +from plone import api +from plone.restapi.services import Service + + +class NavigationService(Service): + """This JSON service reuses the navigation tile to serve through the + REST API the navigation tree of this context. + + The nodes of the tree should be prepared to have a successful + JSON serialization. + + This method removes the keys: + + - brain (which is not serializable) + - parent (which will create a circular reference) + + and renames some keys to match the plone.restapi standards: + + - portal_type -> @type + - url -> @id + + It also fixes the portal_type which is returned normalized + with a dash instead of a dot. + """ + + def fix_node(self, node): + """Prepare a node for serialization""" + banned_keys = [ + "brain", # not serializable + "parent", # creates a circular reference + ] + for key in banned_keys: + if key in node: + del node[key] + + # Rename keys to match plone.restapi standards + mapping = { + "portal_type": "@type", + "url": "@id", + "children": "items", + } + for old_key, new_key in mapping.items(): + if old_key in node: + node[new_key] = node.pop(old_key) + + # Fix the portal_type + if "@type" in node: + node["@type"] = node["@type"].replace("-", ".") + + # Recurse into children + if "items" in node: + for child in node["items"]: + self.fix_node(child) + + return node + + def reply(self): + """We use the navtree tile to get the navigation tree, + but we have to fiddle with the nodes to have a proper serialization. + """ + navtree_tile = api.content.get_view("navtree", self.context, self.request) + navtree_tile.update() + tree = [self.fix_node(node) for node in navtree_tile.tree] + return { + "@id": self.request.getURL(), + "items": tree, + } diff --git a/src/osha/oira/services/surveys.py b/src/osha/oira/services/surveys.py new file mode 100644 index 00000000..f8b53b73 --- /dev/null +++ b/src/osha/oira/services/surveys.py @@ -0,0 +1,85 @@ +from Acquisition import aq_base +from Acquisition import aq_inner +from plone import api +from plone.base.utils import base_hasattr +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.services import Service + + +class ToolVersionsGet(Service): + """Get info from the oira tool (survey group) and its versions (surveys).""" + + @property + def default_image_url(self): + portal_url = api.portal.get().absolute_url() + return f"{portal_url}/++resource++osha.oira.content/clipboard.svg" + + def get_survey_info(self, survey): + # Is this survey the tool version that is published on the client? + published_on_client = self.published_tool_version_id == survey.id + # Note that if 'published_on_client' is true, the review_state + # should be 'published', otherwise 'draft', but this is not always + # in sync. So instead of 'api.content.get_state(obj=survey)', + # let's report what the state is meant to be. + review_state = "published" if published_on_client else "draft" + # The 'published' attribute should be the date of publication of this + # survey, but we could inherit this attribute from the surveygroup, + # where it would contain the id of the client-published tool version. + # So do not inherit this. + published_date = getattr(aq_base(survey), "published", None) + return { + "@id": survey.absolute_url(), + "id": survey.id, + "title": survey.Title(), + "created": json_compatible(survey.created()), + "modified": json_compatible(survey.modified()), + "published": json_compatible(published_date), + "review_state": review_state, + } + + def reply(self): + surveygroup = aq_inner(self.context) + # The 'published' attribute of the surveygroup has the id of the tool + # version that is currently published on the OiRA client side. + self.published_tool_version_id = getattr(surveygroup, "published", None) + + # First gather info about the survey group. + result = { + "@id": f"{surveygroup.absolute_url()}/@tool-versions", + "id": surveygroup.id, + "title": surveygroup.Title(), + "published_tool_version_id": self.published_tool_version_id, + "@type": surveygroup.portal_type, + # We will try to get the next ones from one of the surveys. + "image_url": "", + "summary": "", + "introduction": "", + } + + # Now add info for each tool version. + items = [] + surveys = surveygroup.contentValues() + for survey in surveys: + items.append(self.get_survey_info(survey)) + result["versions"] = items + + # Get some more info from the published survey, or from the first one. + survey = ( + surveygroup.get(self.published_tool_version_id) + if self.published_tool_version_id + else None + ) + if survey is None and surveys: + survey = surveys[0] + if survey is not None: + if base_hasattr(survey, "image") and survey.image: + # The survey has a not inherited image attribute and it is not empty. + result["image_url"] = f"{survey.absolute_url()}/@@images/image" + if base_hasattr(survey, "introduction"): + result["introduction"] = survey.introduction + # The description field always exists. + result["summary"] = survey.Description() + if not result["image_url"]: + result["image_url"] = self.default_image_url + + return result