Volto Blocks support#
The plone.restapi
package gives support for Volto blocks providing a Dexterity behavior plone.restapi.behaviors.IBlocks
.
It is used to enable Volto blocks in any content type.
Volto then renders the blocks engine for all the content types that have this behavior enabled.
Retrieving blocks on a content object#
The plone.restapi.behaviors.IBlocks
has two fields where existing blocks and their data are stored in the object (blocks
).
The one where the current layout is stored (blocks_layout
).
As they are fields in a Dexterity behavior, both fields will be returned in a GET
request as attributes:
GET /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
The server responds with a Status 200
, and lists all stored blocks on that content object:
GET /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"@id": "http://localhost:55001/plone/my-document",
"...more response data...": "",
"blocks_layout": [
"#title-1",
"#description-1",
"#image-1"
],
"blocks": {
"#title-1": {
"@type": "title"
},
"#description-1": {
"@type": "Description"
},
"#image-1": {
"@type": "Image",
"image": "<some random url>"
}
}
}
blocks
objects will contain the title metadata and the information required to render them.
Adding blocks to an object#
Storing blocks is done via a default PATCH
content operation:
PATCH /plone/my-document HTTP/1.1
Accept: application/json
Authorization: Basic YWRtaW46c2VjcmV0
Content-Type: application/json
{
"blocks_layout": [
"#title-1",
"#description-1",
"#image-1"
],
"blocks": {
"#title-1": {
"@type": "title"
},
"#description-1": {
"@type": "Description"
},
"#image-1": {
"@type": "Image",
"image": "<some random url>"
}
}
}
Block serializers and deserializers#
Practical experience has shown that it is useful to transform, server-side, the value of block fields on inbound (deserialization) and also outbound (serialization) operations.
For example, HTML field values are cleaned up using portal_transforms
.
Or paths in image blocks are transformed to use resolveuid
.
It is possible to influence the transformation of block values per block type. For example, to tweak the value stored in an image type block, we can create a new subscriber as follows:
@implementer(IBlockFieldDeserializationTransformer)
@adapter(IBlocks, IBrowserRequest)
class ImageBlockDeserializeTransformer(object):
order = 100
block_type = 'image'
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self, value):
portal = getMultiAdapter(
(self.context, self.request), name="plone_portal_state"
).portal()
url = value.get('url', '')
deserialized_url = path2uid(
context=self.context, portal=portal,
href=url
)
value["url"] = deserialized_url
return value
Then register it as a subscription adapter:
<subscriber factory=".blocks.ImageBlockDeserializeTransformer"
provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"/>
This would replace the url
value to use resolveuid
instead of hard coding the image path.
The block_type
attribute needs to match the @type
field of the block value.
The order
attribute is used in sorting the subscribers for the same field.
A lower number has higher precedence, that is, it is executed first.
On the serialization path, a block value can be tweaked with a similar transformer For example, on an imaginary database listing block type:
@implementer(IBlockFieldDeserializationTransformer)
@adapter(IBlocks, IBrowserRequest)
class DatabaseQueryDeserializeTransformer(object):
order = 100
block_type = 'database_listing'
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self, value):
value["items"] = db.query(value) # pseudocode
return value
Then register it as a subscription adapter:
<subscriber factory=".blocks.DatabaseQueryDeserializeTransformer"
provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer"/>
Generic block transformers and smart fields#
You can create a block transformer that applies to all blocks by using None
as the value for block_type
.
The order
field still applies, though.
The generic block transformers enable us to create smart block fields, which are handled differently.
For example, any internal link stored as url
or href
in a block value is converted (and stored) as a resolveuid
-based URL, then resolved back to a full URL on block serialization.
Another smart field is the searchableText
field in a block value.
It needs to be a plain text value, and it will be used in the SearchableText
value for the context item.
If you need to store "subblocks" in a block value, you should use the blocks
smart field (or data.blocks
).
Doing so integrates those blocks with the transformers.
SearchableText
indexing for blocks#
As the main consumer of plone.restapi
's blocks, this functionality is specific to Volto blocks.
By default, searchable text (for Plone's SearchableText
index) is extracted from text
blocks.
To extract searchable text for other types of blocks, there are two approaches.
Client side solution#
The block provides the data to be indexed in its searchableText
attribute:
{
"@type": "image",
"align": "center",
"alt": "Plone Conference 2021 logo",
"searchableText": "Plone Conference 2021 logo",
"size": "l",
"url": "https://2021.ploneconf.org/images/logoandfamiliesalt.svg"
}
This is the preferred solution.
Server side solution#
For each new block, you need to write an adapter that will extract the searchable text from the block information:
@implementer(IBlockSearchableText)
@adapter(IBlocks, IBrowserRequest)
class ImageSearchableText(object):
def __init__(self, context, request):
self.context = context
self.request = request
def __call__(self, block_value):
return block_value['alt_text']
See plone.restapi.interfaces.IBlockSearchableText
for details.
The __call__
methods needs to return a string, for the text to be indexed.
This adapter needs to be registered as a named adapter, where the name is the same as the block type (its @type
property from the block value):
<adapter name="image" factory=".indexers.ImageBlockSearchableText" />
Visit all blocks#
Since blocks can be contained inside other blocks,
it is not always obvious how to find all of the blocks stored on a content item.
The visit_blocks
utility function will iterate over all blocks:
from plone.restapi.blocks import visit_blocks
for block in visit_blocks(context, context.blocks):
print(block)