Skip to content

Commit

Permalink
WIP Enhance relation field serialization for image content types
Browse files Browse the repository at this point in the history
- In better support of the preview image link
- Reuses serialization from existing field serializers
- Flexible adapation allows new types to be added in the future

WIP until preview_image_link has been merged into plone.volto
  • Loading branch information
reebalazs committed Jun 6, 2022
1 parent 2dbafe5 commit 7adb2a4
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 2 deletions.
3 changes: 3 additions & 0 deletions base.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ parts =
develop = .
sources-dir = extras
auto-checkout =
plone.volto
# plone.rest

allow-hosts =
Expand Down Expand Up @@ -45,6 +46,7 @@ eggs =
Pillow
plone.app.debugtoolbar
plone.restapi [test]
plone.volto
environment-vars =
zope_i18n_compile_mo_files true

Expand Down Expand Up @@ -190,6 +192,7 @@ output = ${buildout:directory}/bin/zpretty-run
mode = 755

[sources]
plone.volto = git git@github.com:plone/plone.volto.git pushurl=git@github.com:plone/plone.volto.git branch=preview-image-link
plone.rest = git git://github.com/plone/plone.rest.git pushurl=git@github.com:plone/plone.rest.git branch=master
plone.schema = git git://github.com/plone/plone.schema.git pushurl=git@github.com:plone/plone.schema.git branch=master
Products.ZCatalog = git git://github.com/zopefoundation/Products.ZCatalog.git pushurl=git@github.com:zopefoundation/Products.ZCatalog.git
1 change: 1 addition & 0 deletions news/xxxx.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Enhance relation field serialization for image content types [@reebalazs]
12 changes: 12 additions & 0 deletions src/plone/restapi/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,15 @@ def non_metadata_attributes():

def blocklisted_attributes():
"""Returns a set with attributes blocked during serialization."""


class IRelationObjectSerializer(Interface):
"""The relation object serializer multi adapter serializes the relation object into
JSON compatible python data.
"""

def __init__(rel_obj, field, context, request):
"""Adapts relation object, field, context and request."""

def __call__():
"""Returns JSON compatible python data."""
5 changes: 5 additions & 0 deletions src/plone/restapi/serializer/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,9 @@
name="plone.restapi.summary_serializer_metadata"
/>

<!-- Relation object serializer -->
<adapter factory=".relationobject.DefaultRelationObjectSerializer" />
<adapter factory=".relationobject.ImageRelationObjectSerializer" />


</configure>
20 changes: 19 additions & 1 deletion src/plone/restapi/serializer/relationfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,25 @@ def relationvalue_converter(value):
@adapter(IRelationChoice, IDexterityContent, Interface)
@implementer(IFieldSerializer)
class RelationChoiceFieldSerializer(DefaultFieldSerializer):
pass
def __call__(self):
result = json_compatible(self.get_value())
# Enhance information based on the content type in relation
if result is None:
return None
portal = getMultiAdapter(
(self.context, self.request), name="plone_portal_state"
).portal()
portal_url = portal.absolute_url()
rel_url = result["@id"]
if not rel_url.startswith(portal_url):
raise RuntimeError(
f"Url must start with portal url. [{portal_url} <> {rel_url}]"
)
rel_path = rel_url[len(portal_url) + 1 :]
rel_obj = portal.unrestrictedTraverse(rel_path, None)
serializer = getMultiAdapter((rel_obj, self.field, self.context, self.request))
result = serializer()
return result


@adapter(IRelationList, IDexterityContent, Interface)
Expand Down
85 changes: 85 additions & 0 deletions src/plone/restapi/serializer/relationobject.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from plone.dexterity.interfaces import IDexterityContent
from plone.restapi.interfaces import IRelationObjectSerializer
from plone.restapi.serializer.converters import json_compatible
from zope.component import adapter
from zope.component import getMultiAdapter
from zope.interface import implementer
from zope.interface import Interface
from zope.interface import alsoProvides
from plone.app.contenttypes.interfaces import IImage
from plone.namedfile.interfaces import INamedImageField
from z3c.relationfield.interfaces import IRelationChoice

import logging


log = logging.getLogger(__name__)


@adapter(IDexterityContent, IRelationChoice, IDexterityContent, Interface)
@implementer(IRelationObjectSerializer)
class DefaultRelationObjectSerializer:
def __init__(self, rel_obj, field, context, request):
self.context = context
self.request = request
self.field = field
self.rel_obj = rel_obj

def __call__(self):
obj = self.rel_obj
# Start from the values of the default field serializer
result = json_compatible(self.get_value())
if result is None:
return None
# Add some more values from the object in relation
additional = {
"id": obj.id,
"created": obj.created(),
"modified": obj.modified(),
"UID": obj.UID(),
}
result.update(additional)
return json_compatible(result)

def get_value(self, default=None):
return getattr(self.field.interface(self.context), self.field.__name__, default)


class FieldSim:
def __init__(self, name, provides):
self.__name__ = name
alsoProvides(self, provides)

def get(self, context):
return getattr(context, self.__name__)


class FieldRelationObjectSerializer(DefaultRelationObjectSerializer):
"""The relationship object is treatable like a field
So we can reuse the serialization for that specific field.
"""

field_name = None
field_interface = None

def __call__(self):
field = FieldSim(self.field_name, self.field_interface)
result = super().__call__()
if result is None:
return None
# Reuse a serializer from dxfields
serializer = getMultiAdapter((field, self.rel_obj, self.request))
# Extend the default values with the content specific ones
additional = serializer()
if additional is not None:
result.update(additional)
return result


@adapter(IImage, IRelationChoice, IDexterityContent, Interface)
class ImageRelationObjectSerializer(FieldRelationObjectSerializer):
# The name of the attribute that contains the data object within the relation object
field_name = "image"
# The field adapter that we will emulate to get the serialized data for this content type
field_interface = INamedImageField
42 changes: 41 additions & 1 deletion src/plone/restapi/tests/test_dxfield_serializer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from DateTime import DateTime
from datetime import date
from datetime import datetime
from datetime import time
Expand Down Expand Up @@ -268,6 +269,9 @@ def test_relationchoice_field_serialization_returns_summary_dict(self):
description="Description 2",
)
]
doc2.creation_date = DateTime("2016-01-21T01:14:48+00:00")
doc2.modification_date = DateTime("2017-01-21T01:14:48+00:00")
doc2_uid = doc2.UID()
value = self.serialize("test_relationchoice_field", doc2)
self.assertEqual(
{
Expand All @@ -276,6 +280,42 @@ def test_relationchoice_field_serialization_returns_summary_dict(self):
"title": "Referenceable Document",
"description": "Description 2",
"review_state": "private",
# Additional fields are added by the relationship serializer
# for default content.
"UID": doc2_uid,
"created": "2016-01-21T01:14:48+00:00",
"id": "doc2",
"modified": "2017-01-21T01:14:48+00:00",
},
value,
)

def test_relationchoice_field_serialization_depends_on_content_type(self):
image1 = self.portal[
self.portal.invokeFactory(
"Image",
id="image1",
title="Test Image",
description="Test Image Description",
)
]
image1.creation_date = DateTime("2016-01-21T01:14:48+00:00")
image1.modification_date = DateTime("2017-01-21T01:14:48+00:00")
image1_uid = image1.UID()
value = self.serialize("test_relationchoice_field", image1)
self.assertEqual(
{
"@id": "http://nohost/plone/image1",
"@type": "Image",
"title": "Test Image",
"description": "Test Image Description",
"review_state": None,
# Additional fields are added by the relationship serializer
# for default content.
"UID": image1_uid,
"created": "2016-01-21T01:14:48+00:00",
"id": "image1",
"modified": "2017-01-21T01:14:48+00:00",
},
value,
)
Expand Down Expand Up @@ -634,5 +674,5 @@ def test_namedblobimage_field_serialization_doesnt_choke_on_corrupt_image(self):


class TestDexterityFieldSerializers(TestCase):
def default_field_serializer(self):
def test_default_field_serializer(self):
verifyClass(IFieldSerializer, DefaultFieldSerializer)
Loading

0 comments on commit 7adb2a4

Please sign in to comment.