From 5a30e9b9defafa664fcc77624c03319f1ec4ed61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 1 Nov 2024 17:27:14 -0300 Subject: [PATCH 1/2] WIP: Serializers/Deserializers for schemas --- news/1832.feature | 1 + src/plone/restapi/deserializer/configure.zcml | 2 + src/plone/restapi/deserializer/dxcontent.py | 132 +----------------- src/plone/restapi/deserializer/dxschema.py | 123 ++++++++++++++++ src/plone/restapi/deserializer/utils.py | 26 ++++ src/plone/restapi/interfaces.py | 24 +++- src/plone/restapi/permissions.py | 25 ++++ src/plone/restapi/profiles/testing/types.xml | 1 - src/plone/restapi/serializer/configure.zcml | 2 + src/plone/restapi/serializer/dxcontent.py | 16 +-- src/plone/restapi/serializer/dxschema.py | 55 ++++++++ src/plone/restapi/serializer/site.py | 44 +----- src/plone/restapi/serializer/utils.py | 12 ++ 13 files changed, 280 insertions(+), 183 deletions(-) create mode 100644 news/1832.feature create mode 100644 src/plone/restapi/deserializer/dxschema.py create mode 100644 src/plone/restapi/serializer/dxschema.py diff --git a/news/1832.feature b/news/1832.feature new file mode 100644 index 0000000000..70b5976371 --- /dev/null +++ b/news/1832.feature @@ -0,0 +1 @@ +Support the registration of serializers and deserializers per schema [@ericof] \ No newline at end of file diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml index f7b4d5c5ac..5d7d516d6d 100644 --- a/src/plone/restapi/deserializer/configure.zcml +++ b/src/plone/restapi/deserializer/configure.zcml @@ -7,6 +7,8 @@ + + diff --git a/src/plone/restapi/deserializer/dxcontent.py b/src/plone/restapi/deserializer/dxcontent.py index dd6bac1851..415d490d7a 100644 --- a/src/plone/restapi/deserializer/dxcontent.py +++ b/src/plone/restapi/deserializer/dxcontent.py @@ -1,28 +1,20 @@ from .mixins import OrderingMixin from AccessControl import getSecurityManager -from plone.autoform.interfaces import WRITE_PERMISSIONS_KEY from plone.dexterity.interfaces import IDexterityContent -from plone.dexterity.utils import iterSchemata from plone.restapi import _ from plone.restapi.deserializer import json_body from plone.restapi.interfaces import IDeserializeFromJson -from plone.restapi.interfaces import IFieldDeserializer -from plone.supermodel.utils import mergedTaggedValueDict -from z3c.form.interfaces import IDataManager +from plone.restapi.deserializer.utils import deserialize_schemas from z3c.form.interfaces import IManagerValidator from zExceptions import BadRequest from zope.component import adapter from zope.component import queryMultiAdapter -from zope.component import queryUtility from zope.event import notify from zope.i18n import translate from zope.interface import implementer from zope.interface import Interface from zope.lifecycleevent import Attributes from zope.lifecycleevent import ObjectModifiedEvent -from zope.schema import getFields -from zope.schema.interfaces import ValidationError -from zope.security.interfaces import IPermission @implementer(IDeserializeFromJson) @@ -34,7 +26,6 @@ def __init__(self, context, request): self.sm = getSecurityManager() self.permission_cache = {} - self.modified = {} def __call__( self, validate_all=False, data=None, create=False, mask_validation_errors=True @@ -43,7 +34,10 @@ def __call__( if data is None: data = json_body(self.request) - schema_data, errors = self.get_schema_data(data, validate_all, create) + # Deserialize JSON + schema_data, errors, modified = deserialize_schemas( + self.context, self.request, data, validate_all, create + ) # Validate schemata for schema, field_data in schema_data.items(): @@ -72,122 +66,10 @@ def __call__( # OrderingMixin self.handle_ordering(data) - if self.modified and not create: + if modified and not create: descriptions = [] - for interface, names in self.modified.items(): + for interface, names in modified.items(): descriptions.append(Attributes(interface, *names)) notify(ObjectModifiedEvent(self.context, *descriptions)) return self.context - - def get_schema_data(self, data, validate_all, create=False): - schema_data = {} - errors = [] - - for schema in iterSchemata(self.context): - write_permissions = mergedTaggedValueDict(schema, WRITE_PERMISSIONS_KEY) - - for name, field in getFields(schema).items(): - __traceback_info__ = f"field={field}" - - field_data = schema_data.setdefault(schema, {}) - - if field.readonly: - continue - - if name in data: - dm = queryMultiAdapter((self.context, field), IDataManager) - if not dm.canWrite(): - continue - - if not self.check_permission(write_permissions.get(name)): - continue - - # set the field to missing_value if we receive null - if data[name] is None: - if not field.required: - if dm.get(): - self.mark_field_as_changed(schema, name) - dm.set(field.missing_value) - else: - errors.append( - { - "field": field.__name__, - "message": _( - "${field_name} is a required field." - " Setting it to null is not allowed.", - mapping={"field_name": field.__name__}, - ), - } - ) - continue - - # Deserialize to field value - deserializer = queryMultiAdapter( - (field, self.context, self.request), IFieldDeserializer - ) - if deserializer is None: - continue - - try: - value = deserializer(data[name]) - except ValueError as e: - errors.append({"message": str(e), "field": name, "error": e}) - except ValidationError as e: - errors.append({"message": e.doc(), "field": name, "error": e}) - else: - field_data[name] = value - current_value = dm.get() - if value != current_value: - should_change = True - elif create and dm.field.defaultFactory: - # During content creation we should set the value even if - # it is the same from the dm if the current_value was - # returned from a default_factory method - should_change = ( - dm.field.defaultFactory(self.context) == current_value - ) - else: - should_change = False - - if should_change: - dm.set(value) - self.mark_field_as_changed(schema, name) - - elif validate_all: - # Never validate the changeNote of p.a.versioningbehavior - # The Versionable adapter always returns an empty string - # which is the wrong type. Should be unicode and should be - # fixed in p.a.versioningbehavior - if name == "changeNote": - continue - dm = queryMultiAdapter((self.context, field), IDataManager) - bound = field.bind(self.context) - try: - bound.validate(dm.get()) - except ValidationError as e: - errors.append({"message": e.doc(), "field": name, "error": e}) - - return schema_data, errors - - def mark_field_as_changed(self, schema, fieldname): - """Collect the names of the modified fields. Use prefixed name because - z3c.form does so. - """ - - prefixed_name = schema.__name__ + "." + fieldname - self.modified.setdefault(schema, []).append(prefixed_name) - - def check_permission(self, permission_name): - if permission_name is None: - return True - - if permission_name not in self.permission_cache: - permission = queryUtility(IPermission, name=permission_name) - if permission is None: - self.permission_cache[permission_name] = True - else: - self.permission_cache[permission_name] = bool( - self.sm.checkPermission(permission.title, self.context) - ) - return self.permission_cache[permission_name] diff --git a/src/plone/restapi/deserializer/dxschema.py b/src/plone/restapi/deserializer/dxschema.py new file mode 100644 index 0000000000..4550aad803 --- /dev/null +++ b/src/plone/restapi/deserializer/dxschema.py @@ -0,0 +1,123 @@ +from plone.autoform.interfaces import WRITE_PERMISSIONS_KEY +from plone.dexterity.interfaces import IDexterityContent +from plone.restapi import _ +from plone.restapi.interfaces import IFieldDeserializer +from plone.restapi.interfaces import ISchemaDeserializer +from plone.restapi.permissions import check_permission +from plone.supermodel.utils import mergedTaggedValueDict +from z3c.form.interfaces import IDataManager +from zope.component import adapter +from zope.component import queryMultiAdapter +from zope.interface import implementer +from zope.interface import Interface +from zope.schema import getFields +from zope.schema.interfaces import ValidationError +from zope.interface.interfaces import IInterface + + +@implementer(ISchemaDeserializer) +@adapter(IInterface, IDexterityContent, Interface) +class DeserializeFromJson: + def __init__(self, schema, context, request): + self.schema = schema + self.context = context + self.request = request + self.permission_cache = {} + self.modified = {} + + def mark_field_as_changed(self, schema, fieldname): + """Collect the names of the modified fields. Use prefixed name because + z3c.form does so. + """ + + prefixed_name = f"{schema.__name__}.{fieldname}" + self.modified.setdefault(schema, []).append(prefixed_name) + + def __call__(self, data, validate_all, create=False) -> tuple[dict, list, dict]: + schema = self.schema + schema_data = {} + errors = [] + write_permissions = mergedTaggedValueDict(schema, WRITE_PERMISSIONS_KEY) + for name, field in getFields(schema).items(): + __traceback_info__ = f"field={field}" + + field_data = schema_data.setdefault(schema, {}) + + if field.readonly: + continue + + if name in data: + dm = queryMultiAdapter((self.context, field), IDataManager) + if not dm.canWrite(): + continue + + if not check_permission( + write_permissions.get(name), self.context, self.permission_cache + ): + continue + + # set the field to missing_value if we receive null + if data[name] is None: + if not field.required: + if dm.get(): + self.mark_field_as_changed(schema, name) + dm.set(field.missing_value) + else: + errors.append( + { + "field": field.__name__, + "message": _( + "${field_name} is a required field." + " Setting it to null is not allowed.", + mapping={"field_name": field.__name__}, + ), + } + ) + continue + + # Deserialize to field value + deserializer = queryMultiAdapter( + (field, self.context, self.request), IFieldDeserializer + ) + if deserializer is None: + continue + + try: + value = deserializer(data[name]) + except ValueError as e: + errors.append({"message": str(e), "field": name, "error": e}) + except ValidationError as e: + errors.append({"message": e.doc(), "field": name, "error": e}) + else: + field_data[name] = value + current_value = dm.get() + if value != current_value: + should_change = True + elif create and dm.field.defaultFactory: + # During content creation we should set the value even if + # it is the same from the dm if the current_value was + # returned from a default_factory method + should_change = ( + dm.field.defaultFactory(self.context) == current_value + ) + else: + should_change = False + + if should_change: + dm.set(value) + self.mark_field_as_changed(schema, name) + elif validate_all: + # Never validate the changeNote of p.a.versioningbehavior + # The Versionable adapter always returns an empty string + # which is the wrong type. Should be unicode and should be + # fixed in p.a.versioningbehavior + if name == "changeNote": + continue + dm = queryMultiAdapter((self.context, field), IDataManager) + bound = field.bind(self.context) + try: + bound.validate(dm.get()) + except ValidationError as e: + errors.append({"message": e.doc(), "field": name, "error": e}) + + return schema_data, errors, self.modified diff --git a/src/plone/restapi/deserializer/utils.py b/src/plone/restapi/deserializer/utils.py index 67d67d5556..5d039e7dee 100644 --- a/src/plone/restapi/deserializer/utils.py +++ b/src/plone/restapi/deserializer/utils.py @@ -1,7 +1,12 @@ from Acquisition import aq_parent +from plone.dexterity.content import DexterityContent +from plone.restapi.interfaces import ISchemaDeserializer from plone.uuid.interfaces import IUUID from plone.uuid.interfaces import IUUIDAware from zope.component import getMultiAdapter +from plone.dexterity.utils import iterSchemata +from zope.component import queryMultiAdapter +from ZPublisher.HTTPRequest import HTTPRequest import re PATH_RE = re.compile(r"^(.*?)((?=/@@|#).*)?$") @@ -50,3 +55,24 @@ def path2uid(context, link): if suffix: href += suffix return href + + +def deserialize_schemas( + context: DexterityContent, + request: HTTPRequest, + data: dict, + validate_all: bool, + create: bool = False, +) -> tuple[dict, list, dict]: + result = {} + errors = [] + modified = {} + for schema in iterSchemata(context): + serializer = queryMultiAdapter((schema, context, request), ISchemaDeserializer) + schema_data, schema_errors, schema_modified = serializer( + data, validate_all, create + ) + result.update(schema_data) + errors.extend(schema_errors) + modified.update(schema_modified) + return result, errors, modified diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 5c2aa337e6..d01909689d 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -32,6 +32,18 @@ def __init__(value, context): """Adapts value and a context""" +class ISchemaSerializer(Interface): + """The schema serializer multi adapter serializes a schema into + JSON compatible python data. + """ + + def __init__(schema, context, request): + """Adapts schema, context and request.""" + + def __call__(): + """Returns JSON compatible python data.""" + + class IFieldSerializer(Interface): """The field serializer multi adapter serializes the field value into JSON compatible python data. @@ -73,11 +85,21 @@ class IDeserializeFromJson(Interface): """An adapter to deserialize a JSON object into an object in Plone.""" +class ISchemaDeserializer(Interface): + """An adapter to deserialize a JSON value from a schema.""" + + def __init__(schema, context, request): + """Adapts schema, context and request.""" + + def __call__(data, validate_all, create): + """Convert the provided JSON value to a field value.""" + + class IFieldDeserializer(Interface): """An adapter to deserialize a JSON value into a field value.""" def __init__(field, context, request): - """Adapts a field, it's context and the request.""" + """Adapts a field, its context and the request.""" def __call__(value): """Convert the provided JSON value to a field value.""" diff --git a/src/plone/restapi/permissions.py b/src/plone/restapi/permissions.py index af1e36b361..968287248e 100644 --- a/src/plone/restapi/permissions.py +++ b/src/plone/restapi/permissions.py @@ -1,6 +1,31 @@ +from plone.dexterity.content import DexterityContent +from zope.security.interfaces import IPermission +from AccessControl import getSecurityManager +from zope.component import queryUtility + # # Required to use the REST API at all, in addition to service specific # permissions. Granted to Anonymous (i.e. everyone) by default via rolemap.xml UseRESTAPI = "plone.restapi: Use REST API" PloneManageUsers = "Plone Site Setup: Users and Groups" + + +def check_permission( + permission_name: str, context: DexterityContent, permission_cache: dict = None +): + if permission_name is None: + return True + elif permission_cache is None: + permission_cache = {} + + if permission_name not in permission_cache: + permission = queryUtility(IPermission, name=permission_name) + if permission is None: + permission_cache[permission_name] = True + else: + sm = getSecurityManager() + permission_cache[permission_name] = bool( + sm.checkPermission(permission.title, context) + ) + return permission_cache[permission_name] diff --git a/src/plone/restapi/profiles/testing/types.xml b/src/plone/restapi/profiles/testing/types.xml index 0202341fac..05b916633f 100644 --- a/src/plone/restapi/profiles/testing/types.xml +++ b/src/plone/restapi/profiles/testing/types.xml @@ -6,5 +6,4 @@ - diff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml index 0e84f64f42..d00981e015 100644 --- a/src/plone/restapi/serializer/configure.zcml +++ b/src/plone/restapi/serializer/configure.zcml @@ -5,6 +5,8 @@ > + + diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index 1c546d091d..0a182dcf52 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -17,6 +17,7 @@ from plone.restapi.serializer.nextprev import NextPrevious from plone.restapi.services.locking import lock_info from plone.restapi.serializer.utils import get_portal_type_title +from plone.restapi.serializer.utils import serialize_schemas from plone.rfc822.interfaces import IPrimaryFieldInfo from plone.supermodel.utils import mergedTaggedValueDict from Products.CMFCore.utils import getToolByName @@ -118,21 +119,8 @@ def __call__(self, version=None, include_items=True): # Insert expandable elements result.update(expandable_elements(self.context, self.request)) - # Insert field values - for schema in iterSchemata(self.context): - read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY) - - for name, field in getFields(schema).items(): - if not self.check_permission(read_permissions.get(name), obj): - continue - - # serialize the field - serializer = queryMultiAdapter( - (field, obj, self.request), IFieldSerializer - ) - value = serializer() - result[json_compatible(name)] = value + result.update(serialize_schemas(obj, self.request)) target_url = getMultiAdapter( (self.context, self.request), IObjectPrimaryFieldTarget diff --git a/src/plone/restapi/serializer/dxschema.py b/src/plone/restapi/serializer/dxschema.py new file mode 100644 index 0000000000..cd446a1445 --- /dev/null +++ b/src/plone/restapi/serializer/dxschema.py @@ -0,0 +1,55 @@ +from plone.autoform.interfaces import READ_PERMISSIONS_KEY +from plone.dexterity.interfaces import IDexterityContent +from plone.restapi.interfaces import IFieldSerializer +from plone.restapi.interfaces import ISchemaSerializer +from plone.restapi.serializer.converters import json_compatible +from plone.restapi.permissions import check_permission +from plone.supermodel.utils import mergedTaggedValueDict +from zope.component import adapter +from zope.component import queryMultiAdapter +from zope.interface import implementer +from zope.interface import Interface +from zope.schema import getFields +from plone.dexterity.interfaces import IDexterityContent +from plone.dexterity.content import DexterityContent +from zope.interface.interfaces import IInterface +from plone.dexterity.interfaces import IContentType +from ZPublisher.HTTPRequest import HTTPRequest + + +class BaseSerializer: + def __init__(self, schema, context: DexterityContent, request: HTTPRequest): + self.schema = schema + self.context = context + self.request = request + self.permission_cache = {} + + def __call__(self): + result = {} + schema = self.schema + read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY) + for name, field in getFields(schema).items(): + if not check_permission( + read_permissions.get(name), self.context, self.permission_cache + ): + continue + + # serialize the field + serializer = queryMultiAdapter( + (field, self.context, self.request), IFieldSerializer + ) + value = serializer() + result[json_compatible(name)] = value + return result + + +@implementer(ISchemaSerializer) +@adapter(IInterface, IDexterityContent, Interface) +class SerializeSchemaToJson(BaseSerializer): + """Serialize ISchema to JSON.""" + + +@implementer(ISchemaSerializer) +@adapter(IContentType, IDexterityContent, Interface) +class DXSchemaSerializeToJson(BaseSerializer): + """Serialize IDexteritySchema to JSON.""" diff --git a/src/plone/restapi/serializer/site.py b/src/plone/restapi/serializer/site.py index 65081fd2ea..b7ae6f5b3b 100644 --- a/src/plone/restapi/serializer/site.py +++ b/src/plone/restapi/serializer/site.py @@ -1,30 +1,20 @@ -from AccessControl import getSecurityManager from importlib import import_module -from plone.autoform.interfaces import READ_PERMISSIONS_KEY -from plone.dexterity.utils import iterSchemata from plone.restapi.batching import HypermediaBatch from plone.restapi.bbb import IPloneSiteRoot from plone.restapi.blocks import iter_block_transform_handlers from plone.restapi.blocks import visit_blocks from plone.restapi.interfaces import IBlockFieldSerializationTransformer -from plone.restapi.interfaces import IFieldSerializer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISerializeToJsonSummary -from plone.restapi.serializer.converters import json_compatible from plone.restapi.serializer.dxcontent import get_allow_discussion_value from plone.restapi.serializer.expansion import expandable_elements -from plone.restapi.serializer.utils import get_portal_type_title +from plone.restapi.serializer.utils import get_portal_type_title, serialize_schemas from plone.restapi.services.locking import lock_info -from plone.supermodel.utils import mergedTaggedValueDict from Products.CMFCore.utils import getToolByName from zope.component import adapter from zope.component import getMultiAdapter -from zope.component import queryMultiAdapter -from zope.component import queryUtility from zope.interface import implementer from zope.interface import Interface -from zope.schema import getFields -from zope.security.interfaces import IPermission import json @@ -40,7 +30,6 @@ class SerializeSiteRootToJson: def __init__(self, context, request): self.context = context self.request = request - self.permission_cache = {} def _build_query(self): path = "/".join(self.context.getPhysicalPath()) @@ -83,21 +72,7 @@ def __call__(self, version=None): ) # Insert Plone Site DX root field values - for schema in iterSchemata(self.context): - read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY) - - for name, field in getFields(schema).items(): - if not self.check_permission( - read_permissions.get(name), self.context - ): - continue - - # serialize the field - serializer = queryMultiAdapter( - (field, self.context, self.request), IFieldSerializer - ) - value = serializer() - result[json_compatible(name)] = value + result.update(serialize_schemas(self.context, self.request)) # Insert locking information result.update({"lock": lock_info(self.context)}) @@ -128,21 +103,6 @@ def __call__(self, version=None): return result - def check_permission(self, permission_name, obj): - if permission_name is None: - return True - - if permission_name not in self.permission_cache: - permission = queryUtility(IPermission, name=permission_name) - if permission is None: - self.permission_cache[permission_name] = True - else: - sm = getSecurityManager() - self.permission_cache[permission_name] = bool( - sm.checkPermission(permission.title, obj) - ) - return self.permission_cache[permission_name] - def serialize_blocks(self): # This is only for below 6 blocks = json.loads(getattr(self.context, "blocks", "{}")) diff --git a/src/plone/restapi/serializer/utils.py b/src/plone/restapi/serializer/utils.py index 89ce0b005a..cadeeb8c6c 100644 --- a/src/plone/restapi/serializer/utils.py +++ b/src/plone/restapi/serializer/utils.py @@ -1,9 +1,13 @@ from plone.app.uuid.utils import uuidToCatalogBrain +from plone.dexterity.content import DexterityContent from plone.dexterity.schema import lookup_fti from plone.restapi.interfaces import IObjectPrimaryFieldTarget from zope.component import queryMultiAdapter from zope.globalrequest import getRequest from zope.i18n import translate +from plone.dexterity.utils import iterSchemata +from plone.restapi.interfaces import ISchemaSerializer +from ZPublisher.HTTPRequest import HTTPRequest import re @@ -53,3 +57,11 @@ def get_portal_type_title(portal_type): if request: return translate(getattr(fti, "Title", lambda: portal_type)(), context=request) return getattr(fti, "Title", lambda: portal_type)() + + +def serialize_schemas(context: DexterityContent, request: HTTPRequest) -> dict: + result = {} + for schema in iterSchemata(context): + serializer = queryMultiAdapter((schema, context, request), ISchemaSerializer) + result.update(serializer()) + return result From 7b781f39e28bccf0bcf2ac3064f3403ae6deaa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Andrei?= Date: Fri, 1 Nov 2024 17:34:39 -0300 Subject: [PATCH 2/2] Lint fixes --- src/plone/restapi/deserializer/dxcontent.py | 1 - src/plone/restapi/serializer/dxcontent.py | 1 - src/plone/restapi/serializer/dxschema.py | 1 - 3 files changed, 3 deletions(-) diff --git a/src/plone/restapi/deserializer/dxcontent.py b/src/plone/restapi/deserializer/dxcontent.py index 415d490d7a..ed1601576f 100644 --- a/src/plone/restapi/deserializer/dxcontent.py +++ b/src/plone/restapi/deserializer/dxcontent.py @@ -1,7 +1,6 @@ from .mixins import OrderingMixin from AccessControl import getSecurityManager from plone.dexterity.interfaces import IDexterityContent -from plone.restapi import _ from plone.restapi.deserializer import json_body from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.deserializer.utils import deserialize_schemas diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index 0a182dcf52..0e598ed5f9 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -7,7 +7,6 @@ from plone.dexterity.utils import iterSchemata from plone.restapi.batching import HypermediaBatch from plone.restapi.deserializer import boolean_value -from plone.restapi.interfaces import IFieldSerializer from plone.restapi.interfaces import IObjectPrimaryFieldTarget from plone.restapi.interfaces import IPrimaryFieldTarget from plone.restapi.interfaces import ISerializeToJson diff --git a/src/plone/restapi/serializer/dxschema.py b/src/plone/restapi/serializer/dxschema.py index cd446a1445..277cd7f9ce 100644 --- a/src/plone/restapi/serializer/dxschema.py +++ b/src/plone/restapi/serializer/dxschema.py @@ -10,7 +10,6 @@ from zope.interface import implementer from zope.interface import Interface from zope.schema import getFields -from plone.dexterity.interfaces import IDexterityContent from plone.dexterity.content import DexterityContent from zope.interface.interfaces import IInterface from plone.dexterity.interfaces import IContentType