Skip to content

Commit be7afaf

Browse files
authored
Add backend support for plate block (#2015)
* Add backend support for plate block * Avoid match statement to support older Pythons :( * Support linkintegrity for plate block * fix * Add tests for resolveuid transforms with plate block * Make order of extracted text consistent in Plone 5 * fix * visit blocks inside plate value
1 parent 92c7631 commit be7afaf

11 files changed

Lines changed: 844 additions & 20 deletions

news/1998.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for plate block from `@kitconcept/volto-plate` (text indexer, resolveuid transforms, link integrity). @davisagli

src/plone/restapi/blocks.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from collections import deque
12
from plone.restapi.interfaces import IBlockVisitor
23
from zope.component import adapter
34
from zope.component import subscribers
@@ -72,3 +73,15 @@ def __call__(self, block_value):
7273
yield from block_value["data"]["blocks"].values()
7374
if "blocks" in block_value:
7475
yield from block_value["blocks"].values()
76+
if block_value.get("@type") == "__somersault__":
77+
yield from self.visit_plate_value(block_value.get("value", []))
78+
79+
def visit_plate_value(self, value):
80+
queue = deque(value)
81+
while queue:
82+
child = queue.pop()
83+
if isinstance(child, dict):
84+
if "@type" in child:
85+
yield child
86+
elif child.get("children", []):
87+
queue.extend(child["children"] or [])

src/plone/restapi/blocks_linkintegrity.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,7 @@ def __call__(self, block):
5454
return links
5555

5656

57-
@adapter(IBlocks, IBrowserRequest)
58-
@implementer(IBlockFieldLinkIntegrityRetriever)
59-
class SlateBlockLinksRetriever:
60-
order = 100
61-
block_type = "slate"
57+
class BaseSlateOrPlateBlockLinksRetriever:
6258
field = "value"
6359

6460
def __init__(self, context, request):
@@ -77,9 +73,16 @@ def __call__(self, block):
7773
value = handler(child)
7874
if value:
7975
self.links.append(value)
80-
8176
return self.links
8277

78+
79+
@adapter(IBlocks, IBrowserRequest)
80+
@implementer(IBlockFieldLinkIntegrityRetriever)
81+
class SlateBlockLinksRetriever(BaseSlateOrPlateBlockLinksRetriever):
82+
order = 100
83+
block_type = "slate"
84+
field = "value"
85+
8386
def handle_a(self, child):
8487
data = child.get("data", {})
8588
if data.get("link", {}).get("internal", {}).get("internal_link"):
@@ -92,6 +95,17 @@ def handle_link(self, child):
9295
return child["data"]["url"]
9396

9497

98+
@adapter(IBlocks, IBrowserRequest)
99+
@implementer(IBlockFieldLinkIntegrityRetriever)
100+
class PlateBlockLinksRetriever(BaseSlateOrPlateBlockLinksRetriever):
101+
order = 100
102+
block_type = "__somersault__"
103+
field = "value"
104+
105+
def handle_a(self, child):
106+
return child.get("url")
107+
108+
95109
@adapter(IBlocks, IBrowserRequest)
96110
@implementer(IBlockFieldLinkIntegrityRetriever)
97111
class GenericBlockLinksRetriever:

src/plone/restapi/configure.zcml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,9 @@
151151
factory=".blocks_linkintegrity.SlateBlockLinksRetriever"
152152
provides="plone.restapi.interfaces.IBlockFieldLinkIntegrityRetriever"
153153
/>
154+
<subscriber
155+
factory=".blocks_linkintegrity.PlateBlockLinksRetriever"
156+
provides="plone.restapi.interfaces.IBlockFieldLinkIntegrityRetriever"
157+
/>
154158

155159
</configure>

src/plone/restapi/indexers.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from plone.indexer.decorator import indexer
33
from plone.restapi import HAS_PLONE_6
44
from plone.restapi.behaviors import IBlocks
5+
from plone.restapi.blocks import visit_blocks
56
from plone.restapi.blocks import visit_subblocks
67
from plone.restapi.interfaces import IBlockSearchableText
8+
from typing import List
79
from zope.component import adapter
810
from zope.component import queryMultiAdapter
911
from zope.globalrequest import getRequest
@@ -65,6 +67,40 @@ def __call__(self, block):
6567
return block.get("plaintext", "")
6668

6769

70+
@implementer(IBlockSearchableText)
71+
@adapter(IBlocks, IBrowserRequest)
72+
class PlateTextIndexer:
73+
"""Searchable Text indexer for plate blocks."""
74+
75+
def __init__(self, context, request):
76+
self.context = context
77+
self.request = request
78+
79+
def __call__(self, block) -> str:
80+
texts = [self.extract_plate_text(block["value"])]
81+
for subblock in visit_subblocks(self.context, block):
82+
texts.append(extract_text(subblock, self.context, self.request))
83+
result = text_strip(texts)
84+
print(result)
85+
return result
86+
87+
def extract_plate_text(self, value) -> str:
88+
if isinstance(value, list):
89+
return " ".join(self.extract_plate_text(item) for item in value)
90+
elif isinstance(value, dict):
91+
if "@type" in value:
92+
# sub-block, will be processed via visit_blocks
93+
return ""
94+
texts = []
95+
for key in ("text", "children"):
96+
if key in value:
97+
texts.append(self.extract_plate_text(value[key]))
98+
return " ".join(texts)
99+
elif isinstance(value, str):
100+
return value.strip()
101+
return ""
102+
103+
68104
def extract_text(block, obj, request):
69105
"""Extract text information from a block.
70106
@@ -93,23 +129,16 @@ def extract_text(block, obj, request):
93129
# Use server side adapters to extract the text data
94130
adapter = queryMultiAdapter((obj, request), IBlockSearchableText, name=block_type)
95131
result = adapter(block) if adapter is not None else ""
96-
if not result:
97-
for subblock in visit_subblocks(obj, block):
98-
tmp_result = extract_text(subblock, obj, request)
99-
result = f"{result}\n{tmp_result}"
100132
return result
101133

102134

103-
def get_blocks_text(obj):
135+
def get_blocks_text(obj) -> List[str]:
104136
"""Extract text to be used by the SearchableText index in the Catalog."""
105137
request = getRequest()
106-
blocks = obj.blocks
107-
blocks_layout = obj.blocks_layout
108-
blocks_text = []
109-
for block_id in blocks_layout.get("items", []):
110-
block = blocks.get(block_id, {})
111-
blocks_text.append(extract_text(block, obj, request))
112-
return blocks_text
138+
texts = []
139+
for block in visit_blocks(obj, obj.blocks):
140+
texts.append(extract_text(block, obj, request))
141+
return texts
113142

114143

115144
def text_strip(text_list):
@@ -139,5 +168,4 @@ def SearchableText_blocks(obj):
139168
blocks_text = get_blocks_text(obj)
140169
# Extract text using the base plone.app.contenttypes indexer
141170
std_text = SearchableText(obj)
142-
blocks_text.append(std_text)
143-
return text_strip(blocks_text)
171+
return text_strip([std_text] + blocks_text)

src/plone/restapi/indexers.zcml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,9 @@
2626
factory=".indexers.TableBlockSearchableText"
2727
name="table"
2828
/>
29+
<adapter
30+
factory=".indexers.PlateTextIndexer"
31+
name="__somersault__"
32+
/>
2933

3034
</configure>

src/plone/restapi/tests/test_blocks.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,21 @@ def test_visit_blocks(self):
3838
visited.append(block["@id"])
3939
# depth-first traversal
4040
self.assertEqual(visited, ["block2", "block1"])
41+
42+
def test_visit_blocks_in_plate_value(self):
43+
visited = []
44+
blocks = {
45+
"__somersault__": {
46+
"@type": "__somersault__",
47+
"value": [
48+
{
49+
"type": "p",
50+
"children": [{"@type": "image"}],
51+
}
52+
],
53+
}
54+
}
55+
for block in visit_blocks(self.doc, blocks):
56+
visited.append(block["@type"])
57+
# depth-first traversal
58+
self.assertEqual(visited, ["image", "__somersault__"])

src/plone/restapi/tests/test_blocks_deserializer.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -738,3 +738,33 @@ def test_deserializer_resolve_path_also_if_it_is_an_alias(self):
738738
res = self.deserialize(blocks=blocks)
739739
link = res.blocks["abc"]["href"]
740740
self.assertEqual(link, f"../resolveuid/{self.portal['renamed-doc'].UID()}")
741+
742+
def test_plate_internal_link_deserializer(self):
743+
target_url = self.image.absolute_url()
744+
blocks = {
745+
"__somersault__": {
746+
"@type": "__somersault__",
747+
"value": [
748+
{
749+
"blockWidth": "default",
750+
"children": [
751+
{"text": ""},
752+
{
753+
"blockWidth": "default",
754+
"children": [{"text": "This is a link"}],
755+
"id": "LR0yRMwNPU",
756+
"type": "a",
757+
"url": target_url,
758+
},
759+
{"text": ""},
760+
],
761+
"id": "IR4xX0-rK0",
762+
"type": "p",
763+
},
764+
],
765+
},
766+
}
767+
res = self.deserialize(blocks=blocks)
768+
value = res.blocks["__somersault__"]["value"]
769+
link = value[0]["children"][1]["url"]
770+
self.assertTrue(link.startswith("../resolveuid/"))

src/plone/restapi/tests/test_blocks_linkintegrity.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,39 @@ def test_links_retriever_skip_empty_links(self):
248248

249249
self.assertEqual(len(value), 0)
250250

251+
def test_links_retriever_return_internal_links_type_a_in_plate_block(self):
252+
uid = IUUID(self.doc2)
253+
resolve_uid_link = f"../resolveuid/{uid}"
254+
blocks = {
255+
"__somersault__": {
256+
"@type": "__somersault__",
257+
"value": [
258+
{
259+
"blockWidth": "default",
260+
"children": [
261+
{"text": ""},
262+
{
263+
"blockWidth": "default",
264+
"children": [{"text": "This is a link"}],
265+
"id": "LR0yRMwNPU",
266+
"type": "a",
267+
"url": resolve_uid_link,
268+
},
269+
{"text": ""},
270+
],
271+
"id": "IR4xX0-rK0",
272+
"type": "p",
273+
},
274+
],
275+
},
276+
}
277+
278+
self.portal.doc1.blocks = blocks
279+
value = self.retrieve_links(blocks)
280+
281+
self.assertEqual(len(value), 1)
282+
self.assertIn(resolve_uid_link, value)
283+
251284

252285
class TestLinkintegrityForBlocks(TestCase):
253286

src/plone/restapi/tests/test_blocks_serializer.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,3 +637,38 @@ def test_teaser_block_serializer_legacy(self):
637637
self.assertEqual(block["description"], "Custom description")
638638
href = block["href"][0]
639639
self.assertEqual(href["@id"], doc.absolute_url())
640+
641+
def test_plate_internal_link_serializer(self):
642+
target_item = self.image
643+
resolveuid_link = f"../resolveuid/{target_item.UID()}"
644+
blocks = {
645+
"__somersault__": {
646+
"@type": "__somersault__",
647+
"value": [
648+
{
649+
"blockWidth": "default",
650+
"children": [
651+
{"text": ""},
652+
{
653+
"blockWidth": "default",
654+
"children": [{"text": "This is a link"}],
655+
"id": "LR0yRMwNPU",
656+
"type": "a",
657+
"url": resolveuid_link,
658+
},
659+
{"text": ""},
660+
],
661+
"id": "IR4xX0-rK0",
662+
"type": "p",
663+
},
664+
],
665+
},
666+
}
667+
668+
res = self.serialize(
669+
context=self.portal["doc1"],
670+
blocks=blocks,
671+
)
672+
value = res["__somersault__"]["value"]
673+
link = value[0]["children"][1]["url"]
674+
self.assertTrue(link == target_item.absolute_url())

0 commit comments

Comments
 (0)