diff --git a/news/1788.feature b/news/1788.feature new file mode 100644 index 0000000000..b245234466 --- /dev/null +++ b/news/1788.feature @@ -0,0 +1 @@ +Added TeaserBlockSerializer which updates the contents of a teaser block from its target if the block has `"overwrite": false`. @pbauer, @davisagli \ No newline at end of file diff --git a/src/plone/restapi/serializer/blocks.py b/src/plone/restapi/serializer/blocks.py index d36d0bc0e4..da1c4a2e62 100644 --- a/src/plone/restapi/serializer/blocks.py +++ b/src/plone/restapi/serializer/blocks.py @@ -1,3 +1,5 @@ +from plone import api +from plone.app.uuid.utils import uuidToCatalogBrain from plone.restapi.bbb import IPloneSiteRoot from plone.restapi.behaviors import IBlocks from plone.restapi.blocks import visit_blocks, iter_block_transform_handlers @@ -6,17 +8,20 @@ from plone.restapi.deserializer.blocks import transform_links from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import IFieldSerializer +from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.serializer.converters import json_compatible from plone.restapi.serializer.dxfields import DefaultFieldSerializer from plone.restapi.serializer.utils import resolve_uid, uid_to_url from plone.schema import IJSONField from zope.component import adapter +from zope.component import getMultiAdapter from zope.interface import implementer from zope.interface import Interface from zope.publisher.interfaces.browser import IBrowserRequest import copy import os +import re @adapter(IJSONField, IBlocks, Interface) @@ -193,3 +198,81 @@ class SlateTableBlockSerializer(SlateTableBlockSerializerBase): @adapter(IPloneSiteRoot, IBrowserRequest) class SlateTableBlockSerializerRoot(SlateTableBlockSerializerBase): """Serializer for site root""" + + +class TeaserBlockSerializerBase: + order = 0 + block_type = "teaser" + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, block): + return self._process_data(block) + + def _process_data(self, data, field=None): + value = data.get("href", "") + if value: + if "overwrite" not in data: + # A block without this option is old and keeps the behavior + # where data is not dynamically pulled from the href + data["overwrite"] = True + return data + + if isinstance(value, str): + url = value + value = [{"@id": url}] + else: + url = value[0].get("@id", "") + brain = url_to_brain(url) + if brain is not None: + serialized_brain = getMultiAdapter( + (brain, self.request), ISerializeToJsonSummary + )() + + if not data.get("overwrite"): + # Update fields at the top level of the block data + for key in ["title", "description", "head_title"]: + if key in serialized_brain: + data[key] = serialized_brain[key] + + # We return the serialized brain. + value[0].update(serialized_brain) + data["href"] = value + elif not url.startswith("http"): + # Source not found; clear out derived fields + data["href"] = [] + return data + + +@implementer(IBlockFieldSerializationTransformer) +@adapter(IBlocks, IBrowserRequest) +class TeaserBlockSerializer(TeaserBlockSerializerBase): + """Serializer for content-types with IBlocks behavior""" + + +RESOLVE_UID_REGEXP = re.compile("resolveuid/([^/]+)") + + +@implementer(IBlockFieldSerializationTransformer) +@adapter(IPloneSiteRoot, IBrowserRequest) +class TeaserBlockSerializerRoot(TeaserBlockSerializerBase): + """Serializer for site root""" + + +def url_to_brain(url): + if not url: + return + brain = None + if match := RESOLVE_UID_REGEXP.search(url): + uid = match.group(1) + brain = uuidToCatalogBrain(uid) + else: + # fallback in case the url wasn't converted to a UID + catalog = api.portal.get_tool("portal_catalog") + path = "/".join(api.portal.get().getPhysicalPath()) + url + results = catalog.searchResults(path={"query": path, "depth": 0}) + if results: + brain = results[0] + return brain diff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml index c1117121ae..0e84f64f42 100644 --- a/src/plone/restapi/serializer/configure.zcml +++ b/src/plone/restapi/serializer/configure.zcml @@ -63,6 +63,14 @@ provides="plone.restapi.interfaces.IBlockFieldSerializationTransformer" /> + + diff --git a/src/plone/restapi/tests/test_blocks_serializer.py b/src/plone/restapi/tests/test_blocks_serializer.py index 9005e2a406..a1c378ad84 100644 --- a/src/plone/restapi/tests/test_blocks_serializer.py +++ b/src/plone/restapi/tests/test_blocks_serializer.py @@ -517,3 +517,97 @@ def test_image_scales_serializer_is_json_compatible(self): blocks={"123": {"@type": "image", "url": f"../resolveuid/{image_uid}"}}, ) self.assertIs(type(res["123"]["image_scales"]), dict) + + def test_teaser_block_serializer_dynamic(self): + doc = self.portal["doc1"] + doc_uid = doc.UID() + resolve_uid_link = f"../resolveuid/{doc_uid}" + value = self.serialize( + context=self.portal.doc1, + blocks={ + "1": { + "@type": "teaser", + "href": resolve_uid_link, + "overwrite": False, + } + }, + ) + + block = value["1"] + self.assertEqual(block["title"], doc.title) + self.assertEqual(block["description"], doc.description) + href = block["href"][0] + self.assertEqual(href["@id"], doc.absolute_url()) + + def test_teaser_block_serializer_dynamic_nested(self): + doc = self.portal["doc1"] + doc_uid = doc.UID() + resolve_uid_link = f"../resolveuid/{doc_uid}" + value = self.serialize( + context=self.portal.doc1, + blocks={ + "grid": { + "@type": "gridBlock", + "blocks": { + "1": { + "@type": "teaser", + "href": resolve_uid_link, + "overwrite": False, + }, + }, + "blocks_layout": {"items": ["1"]}, + } + }, + ) + + block = value["grid"]["blocks"]["1"] + self.assertEqual(block["title"], doc.title) + self.assertEqual(block["description"], doc.description) + href = block["href"][0] + self.assertEqual(href["@id"], doc.absolute_url()) + + def test_teaser_block_serializer_with_overwrite(self): + doc = self.portal["doc1"] + doc_uid = doc.UID() + resolve_uid_link = f"../resolveuid/{doc_uid}" + value = self.serialize( + context=self.portal.doc1, + blocks={ + "1": { + "@type": "teaser", + "href": resolve_uid_link, + "overwrite": True, + "title": "Custom title", + "description": "Custom description", + } + }, + ) + + block = value["1"] + self.assertEqual(block["title"], "Custom title") + self.assertEqual(block["description"], "Custom description") + href = block["href"][0] + self.assertEqual(href["@id"], doc.absolute_url()) + + def test_teaser_block_serializer_legacy(self): + # no "overwrite" key -> default to True + doc = self.portal["doc1"] + doc_uid = doc.UID() + resolve_uid_link = f"../resolveuid/{doc_uid}" + value = self.serialize( + context=self.portal.doc1, + blocks={ + "1": { + "@type": "teaser", + "href": [{"@id": resolve_uid_link}], + "title": "Custom title", + "description": "Custom description", + } + }, + ) + + block = value["1"] + self.assertEqual(block["title"], "Custom title") + self.assertEqual(block["description"], "Custom description") + href = block["href"][0] + self.assertEqual(href["@id"], doc.absolute_url())