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

WIP Enhance relation field serialization for image content types #1442

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really need to tie this to the plone.volto change. So I'd remove this for the merge.

# 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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting approach, wondering how this will scale in the case that we have a custom content type and we want to return, let's say two images.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or a combination of fields. Not sure if make a 1:1 content type => field serialised I would like to include in the final serialization is a good choice.

# 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