Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Teaser blocks dynamic #1788

Merged
merged 11 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/1788.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make Teaser blocks dynamic @pbauer
davisagli marked this conversation as resolved.
Show resolved Hide resolved
83 changes: 83 additions & 0 deletions src/plone/restapi/serializer/blocks.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -193,3 +198,81 @@ class SlateTableBlockSerializer(SlateTableBlockSerializerBase):
@adapter(IPloneSiteRoot, IBrowserRequest)
class SlateTableBlockSerializerRoot(SlateTableBlockSerializerBase):
"""Serializer for site root"""


class TeaserBlockSerializerBase:
order = 0
davisagli marked this conversation as resolved.
Show resolved Hide resolved
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
8 changes: 8 additions & 0 deletions src/plone/restapi/serializer/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@
provides="plone.restapi.interfaces.IBlockFieldSerializationTransformer"
/>

<subscriber
factory=".blocks.TeaserBlockSerializerRoot"
provides="plone.restapi.interfaces.IBlockFieldSerializationTransformer"
/>
<subscriber
factory=".blocks.TeaserBlockSerializer"
provides="plone.restapi.interfaces.IBlockFieldSerializationTransformer"
/>

<adapter factory=".converters.date_converter" />
<adapter factory=".converters.decimal_converter" />
Expand Down
94 changes: 94 additions & 0 deletions src/plone/restapi/tests/test_blocks_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Loading