From 24b3649d39846ce4ba1fb80cf21cbb488c783f62 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 4 Feb 2021 17:45:23 +0200 Subject: [PATCH 01/99] WIP --- src/plone/restapi/configure.zcml | 66 ++++-- src/plone/restapi/interfaces.py | 37 +++ src/plone/restapi/permissions.zcml | 12 +- .../restapi/profiles/default/metadata.xml | 2 +- src/plone/restapi/serializer/blocks.py | 3 +- src/plone/restapi/serializer/configure.zcml | 216 +++++++++--------- src/plone/restapi/serializer/slots.py | 69 ++++++ src/plone/restapi/services/configure.zcml | 77 ++++--- src/plone/restapi/services/slots/__init__.py | 0 .../restapi/services/slots/configure.zcml | 57 +++++ src/plone/restapi/services/slots/get.py | 49 ++++ src/plone/restapi/services/slots/update.py | 61 +++++ src/plone/restapi/services/tiles/get.py | 2 - src/plone/restapi/slots.py | 29 +++ .../upgrades/profiles/0007/rolemap.xml | 8 + 15 files changed, 530 insertions(+), 158 deletions(-) create mode 100644 src/plone/restapi/serializer/slots.py create mode 100644 src/plone/restapi/services/slots/__init__.py create mode 100644 src/plone/restapi/services/slots/configure.zcml create mode 100644 src/plone/restapi/services/slots/get.py create mode 100644 src/plone/restapi/services/slots/update.py create mode 100644 src/plone/restapi/slots.py create mode 100644 src/plone/restapi/upgrades/profiles/0007/rolemap.xml diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index ae4a931da9..5757ba550a 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -1,76 +1,89 @@ + i18n_domain="plone.restapi" + > - - + + - + + name="plone.restapi" + title="plone.restapi special import handlers" + description="" + handler="plone.restapi.setuphandlers.import_various" + /> - + @@ -81,21 +94,36 @@ - + + /> + + + + diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 41a73d000b..89dc662cac 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -4,10 +4,13 @@ # E0211: Method has no argument # W0221: Arguments number differs from overridden '__call__' method +from plone.schema import JSONField from zope.interface import Attribute from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer +import json + class IPloneRestapiLayer(IDefaultBrowserLayer): """Marker interface that defines a browser layer.""" @@ -210,3 +213,37 @@ def __init__(field, context, request): def __call__(value): """Extract text from the block value. Returns text""" + + +class ISlots(Interface): + """Slots are named container of sets of blocks""" + + +BLOCKS_SCHEMA = json.dumps({"type": "object", "properties": {}}) + +LAYOUT_SCHEMA = json.dumps( + { + "type": "object", + "properties": {"items": {"type": "array", "items": {"type": "string"}}}, + } +) + + +class ISlot(Interface): + """Slots follow the IBlocks model""" + + slot_blocks = JSONField( + title=u"Slot blocks", + description=u"The JSON representation of the slot blocks information. Must be a JSON object.", # noqa + schema=BLOCKS_SCHEMA, + default={}, + required=False, + ) + + slot_blocks_layout = JSONField( + title=u"Slot blocks Layout", + description=u"The JSON representation of the slot blocks layout. Must be a JSON array.", # noqa + schema=LAYOUT_SCHEMA, + default={"items": []}, + required=False, + ) diff --git a/src/plone/restapi/permissions.zcml b/src/plone/restapi/permissions.zcml index 8dcd48e81c..76989540e7 100644 --- a/src/plone/restapi/permissions.zcml +++ b/src/plone/restapi/permissions.zcml @@ -1,6 +1,7 @@ + i18n_domain="plone.restapi" + > + + + diff --git a/src/plone/restapi/profiles/default/metadata.xml b/src/plone/restapi/profiles/default/metadata.xml index 81970e34ed..1e4872e498 100644 --- a/src/plone/restapi/profiles/default/metadata.xml +++ b/src/plone/restapi/profiles/default/metadata.xml @@ -1,4 +1,4 @@ - 0006 + 0007 diff --git a/src/plone/restapi/serializer/blocks.py b/src/plone/restapi/serializer/blocks.py index b00f35accb..08f8a82104 100644 --- a/src/plone/restapi/serializer/blocks.py +++ b/src/plone/restapi/serializer/blocks.py @@ -17,8 +17,9 @@ from zope.publisher.interfaces.browser import IBrowserRequest import copy -import re import os +import re + RESOLVEUID_RE = re.compile("^[./]*resolve[Uu]id/([^/]*)/?(.*)$") diff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml index 778c173a8f..b24e5eb332 100644 --- a/src/plone/restapi/serializer/configure.zcml +++ b/src/plone/restapi/serializer/configure.zcml @@ -1,109 +1,121 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + i18n_domain="plone.restapi" + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py new file mode 100644 index 0000000000..2084a5fcef --- /dev/null +++ b/src/plone/restapi/serializer/slots.py @@ -0,0 +1,69 @@ +from plone.restapi.interfaces import IBlockFieldSerializationTransformer +from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.interfaces import ISlot +from plone.restapi.interfaces import ISlots +from plone.restapi.serializer.converters import json_compatible +from zope.component import adapter +from zope.component import getMultiAdapter +from zope.component import subscribers +from zope.interface import implementer +from zope.interface import Interface + +import copy + + +SERVICE_ID = "@slots" + + +@adapter(Interface, ISlot, Interface) +@implementer(ISerializeToJson) +class SlotSerializer(object): + """Default serializer for a single persistent slot""" + + def __init__(self, context, slot, request): + self.context = context + self.request = request + self.slot = slot + + def __call__(self, name): + slot_blocks = copy.deepcopy(self.slot_blocks) + slot_blocks_layout = copy.deepcopy(self.slot_blocks_layout) + + for id, block_value in slot_blocks.items(): + block_type = block_value.get("@type", "") + handlers = [] + for h in subscribers( + (self.context, self.request), IBlockFieldSerializationTransformer + ): + if h.block_type == block_type or h.block_type is None: + handlers.append(h) + + for handler in sorted(handlers, key=lambda h: h.order): + if not getattr(handler, "disabled", False): + block_value = handler(block_value) + + slot_blocks[id] = json_compatible(block_value) + + return { + "@id": "{0}/{1}/{2}".format(self.context.absolute_url, SERVICE_ID, name), + "slot_blocks": slot_blocks, + "slot_blocks_layout": slot_blocks_layout, + } + + +@adapter(Interface, ISlots, Interface) +@implementer(ISerializeToJson) +class SlotsSerializer(object): + """Default slots storage serializer""" + + def __call__(self): + result = [] + storage = ISlots(self.context) + + for name, slot in storage.items(): + serializer = getMultiAdapter( + (self.context, slot, self.request), ISerializeToJson + ) + result.append(serializer(name)) + + return result diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml index 046bef933b..348ae3666a 100644 --- a/src/plone/restapi/services/configure.zcml +++ b/src/plone/restapi/services/configure.zcml @@ -1,44 +1,57 @@ + xmlns:zcml="http://namespaces.zope.org/zcml" + > - + - - + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/src/plone/restapi/services/slots/__init__.py b/src/plone/restapi/services/slots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/plone/restapi/services/slots/configure.zcml b/src/plone/restapi/services/slots/configure.zcml new file mode 100644 index 0000000000..a04c959b88 --- /dev/null +++ b/src/plone/restapi/services/slots/configure.zcml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py new file mode 100644 index 0000000000..4805cadafd --- /dev/null +++ b/src/plone/restapi/services/slots/get.py @@ -0,0 +1,49 @@ +from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.interfaces import ISlots +from plone.restapi.services import Service +from zope.component import getMultiAdapter +from zope.interface import implementer +from zope.publisher.interfaces import IPublishTraverse + + +@implementer(IPublishTraverse) +class SlotsGet(Service): + """Returns the available slots.""" + + def __init__(self, context, request): + super(SlotsGet, self).__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + self.params.append(name) + return self + + def reply(self): + storage = ISlots(self.context) + + if self.params and len(self.params) > 0: + return self.replySlot() + + storage = ISlots(self.context) + + adapter = getMultiAdapter( + (self.context, storage, self.request), ISerializeToJson + ) + + return adapter() + + def replySlot(self): + name = self.params[0] + storage = ISlots(self.context) + + try: + slot = storage[name] + return getMultiAdapter( + (self.context, slot, self.request), ISerializeToJson + )(name) + except KeyError: + self.request.response.setStatus(404) + return { + "type": "NotFound", + "message": 'Tile "{}" could not be found.'.format(self.params[0]), + } diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py new file mode 100644 index 0000000000..de1e2045f7 --- /dev/null +++ b/src/plone/restapi/services/slots/update.py @@ -0,0 +1,61 @@ +""" Slot patch operations +""" + +from plone.restapi.exceptions import DeserializationError +from plone.restapi.interfaces import IDeserializeFromJson +from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.services import Service +from plone.restapi.services.locking.locking import is_locked +from zope.component import queryMultiAdapter + + +class SlotsPatch(Service): + """Update one or all the slots""" + + def __init__(self, context, request): + super(SlotsPatch, self).__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + self.params.append(name) + return self + + def reply(self): + if self.params and len(self.params) > 0: + return self.replySlot() + + if is_locked(self.context, self.request): + self.request.response.setStatus(403) + return dict(error=dict(type="Forbidden", message="Resource is locked.")) + + deserializer = queryMultiAdapter( + (self.context, self.request), IDeserializeFromJson + ) + if deserializer is None: + self.request.response.setStatus(501) + return dict( + error=dict( + message="Cannot deserialize type {}".format( + self.context.portal_type + ) + ) + ) + + try: + deserializer() + except DeserializationError as e: + self.request.response.setStatus(400) + return dict(error=dict(type="DeserializationError", message=str(e))) + + prefer = self.request.getHeader("Prefer") + if prefer == "return=representation": + self.request.response.setStatus(200) + + serializer = queryMultiAdapter( + (self.context, self.request), ISerializeToJson + ) + + serialized_obj = serializer() + return serialized_obj + + return self.reply_no_content() diff --git a/src/plone/restapi/services/tiles/get.py b/src/plone/restapi/services/tiles/get.py index 06549945a4..3f58846b48 100644 --- a/src/plone/restapi/services/tiles/get.py +++ b/src/plone/restapi/services/tiles/get.py @@ -24,12 +24,10 @@ def publishTraverse(self, request, name): def reply(self): if self.params and len(self.params) > 0: - self.content_type = "application/json+schema" try: tile = getUtility(ITileType, name=self.params[0]) return getMultiAdapter((tile, self.request), ISerializeToJson)() except KeyError: - self.content_type = "application/json" self.request.response.setStatus(404) return { "type": "NotFound", diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py new file mode 100644 index 0000000000..06124f505b --- /dev/null +++ b/src/plone/restapi/slots.py @@ -0,0 +1,29 @@ +from .interfaces import ISlot +from .interfaces import ISlots +from persistent import Persistent +from persistent.mapping import PersistentMapping +from Products.CMFCore.interfaces import IContentish +from zope.annotation.factory import factory +from zope.component import adapter +from zope.interface import implementer + + +SLOTS_KEY = "plone.restapi.slots" + + +@adapter(IContentish) +@implementer(ISlots) +class PersistentSlots(PersistentMapping): + """Slots store""" + + +Slots = factory(PersistentSlots, SLOTS_KEY) + + +@implementer(ISlot) +class Slot(Persistent): + """A container for data pertaining to a single slot""" + + def __init__(self): + self.slot_blocks_layout = {} + self.slot_blocks = {} diff --git a/src/plone/restapi/upgrades/profiles/0007/rolemap.xml b/src/plone/restapi/upgrades/profiles/0007/rolemap.xml new file mode 100644 index 0000000000..ab702a8638 --- /dev/null +++ b/src/plone/restapi/upgrades/profiles/0007/rolemap.xml @@ -0,0 +1,8 @@ + + + + + + + + From 8aeec3cf9ed6d62340dbf241cac5bad0a9196be4 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 4 Feb 2021 18:16:50 +0200 Subject: [PATCH 02/99] Add slot_block_ids index --- src/plone/restapi/indexers.py | 17 +++++++++++++++++ src/plone/restapi/indexers.zcml | 19 +++++++++++++++---- src/plone/restapi/interfaces.py | 8 ++++---- src/plone/restapi/serializer/slots.py | 1 + src/plone/restapi/services/slots/get.py | 1 + src/plone/restapi/services/slots/update.py | 2 ++ src/plone/restapi/slots.py | 1 + .../upgrades/profiles/0007/catalog.xml | 8 ++++++++ 8 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 src/plone/restapi/upgrades/profiles/0007/catalog.xml diff --git a/src/plone/restapi/indexers.py b/src/plone/restapi/indexers.py index 588f126b87..6c3f9e5570 100644 --- a/src/plone/restapi/indexers.py +++ b/src/plone/restapi/indexers.py @@ -10,6 +10,10 @@ from plone.indexer.decorator import indexer from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockSearchableText +from plone.restapi.interfaces import ISlots +from plone.restapi.slots import SLOTS_KEY +from Products.CMFCore.interfaces import IContentish +from zope.annotation.interfaces import IAnnotations from zope.component import adapter from zope.component import queryMultiAdapter from zope.globalrequest import getRequest @@ -73,3 +77,16 @@ def SearchableText_blocks(obj): blocks_text.append(std_text) return " ".join(blocks_text) + + +@indexer(IContentish) +def slot_block_ids(obj): + if SLOTS_KEY not in IAnnotations(obj): + return + + blocks = [] + storage = ISlots(obj) + for name, slot in storage.items(): + blocks.extend(slot.slot_blocks_layout['items']) + + return blocks diff --git a/src/plone/restapi/indexers.zcml b/src/plone/restapi/indexers.zcml index 4a3c2cd22b..5f47f838e6 100644 --- a/src/plone/restapi/indexers.zcml +++ b/src/plone/restapi/indexers.zcml @@ -1,12 +1,23 @@ + i18n_domain="plone" + > - - + + - + + diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 89dc662cac..2081c25b74 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -219,9 +219,9 @@ class ISlots(Interface): """Slots are named container of sets of blocks""" -BLOCKS_SCHEMA = json.dumps({"type": "object", "properties": {}}) +SLOT_BLOCKS_SCHEMA = json.dumps({"type": "object", "properties": {}}) -LAYOUT_SCHEMA = json.dumps( +SLOT_LAYOUT_SCHEMA = json.dumps( { "type": "object", "properties": {"items": {"type": "array", "items": {"type": "string"}}}, @@ -235,7 +235,7 @@ class ISlot(Interface): slot_blocks = JSONField( title=u"Slot blocks", description=u"The JSON representation of the slot blocks information. Must be a JSON object.", # noqa - schema=BLOCKS_SCHEMA, + schema=SLOT_BLOCKS_SCHEMA, default={}, required=False, ) @@ -243,7 +243,7 @@ class ISlot(Interface): slot_blocks_layout = JSONField( title=u"Slot blocks Layout", description=u"The JSON representation of the slot blocks layout. Must be a JSON array.", # noqa - schema=LAYOUT_SCHEMA, + schema=SLOT_LAYOUT_SCHEMA, default={"items": []}, required=False, ) diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 2084a5fcef..c073c2eed4 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISlot diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 4805cadafd..f5f2342896 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISlots from plone.restapi.services import Service diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index de1e2045f7..83d01ebf12 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """ Slot patch operations """ diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 06124f505b..de639f25f6 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from .interfaces import ISlot from .interfaces import ISlots from persistent import Persistent diff --git a/src/plone/restapi/upgrades/profiles/0007/catalog.xml b/src/plone/restapi/upgrades/profiles/0007/catalog.xml new file mode 100644 index 0000000000..038792d169 --- /dev/null +++ b/src/plone/restapi/upgrades/profiles/0007/catalog.xml @@ -0,0 +1,8 @@ + + + + + + + + From 0302833923990e4751de81c8b83eaf2c2bfb12fa Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 4 Feb 2021 18:25:05 +0200 Subject: [PATCH 03/99] Improve patch service --- src/plone/restapi/services/slots/update.py | 55 +++++++++++++++------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index 83d01ebf12..72726b28b5 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -6,9 +6,10 @@ from plone.restapi.exceptions import DeserializationError from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.interfaces import ISlots from plone.restapi.services import Service from plone.restapi.services.locking.locking import is_locked -from zope.component import queryMultiAdapter +from zope.component import getMultiAdapter class SlotsPatch(Service): @@ -23,25 +24,18 @@ def publishTraverse(self, request, name): return self def reply(self): - if self.params and len(self.params) > 0: - return self.replySlot() - if is_locked(self.context, self.request): self.request.response.setStatus(403) return dict(error=dict(type="Forbidden", message="Resource is locked.")) - deserializer = queryMultiAdapter( - (self.context, self.request), IDeserializeFromJson + if self.params and len(self.params) > 0: + return self.replySlot() + + storage = ISlots(self.context) + + deserializer = getMultiAdapter( + (self.context, storage, self.request), IDeserializeFromJson ) - if deserializer is None: - self.request.response.setStatus(501) - return dict( - error=dict( - message="Cannot deserialize type {}".format( - self.context.portal_type - ) - ) - ) try: deserializer() @@ -53,11 +47,38 @@ def reply(self): if prefer == "return=representation": self.request.response.setStatus(200) - serializer = queryMultiAdapter( - (self.context, self.request), ISerializeToJson + serializer = getMultiAdapter( + (self.context, storage, self.request), ISerializeToJson ) serialized_obj = serializer() return serialized_obj return self.reply_no_content() + + def replySlot(self): + name = self.params[0] + storage = ISlots(self.context) + slot = storage[name] + + deserializer = getMultiAdapter( + (self.context, slot, self.request), IDeserializeFromJson + ) + try: + deserializer() + except DeserializationError as e: + self.request.response.setStatus(400) + return dict(error=dict(type="DeserializationError", message=str(e))) + + prefer = self.request.getHeader("Prefer") + if prefer == "return=representation": + self.request.response.setStatus(200) + + serializer = getMultiAdapter( + (self.context, slot, self.request), ISerializeToJson + ) + + serialized_obj = serializer(name) + return serialized_obj + + return self.reply_no_content() From 85b9fa73ac9632abd237ca54b09d63a824e0cfae Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 4 Feb 2021 22:59:51 +0200 Subject: [PATCH 04/99] WIP on deserializers --- src/plone/restapi/deserializer/configure.zcml | 66 +++++++---- src/plone/restapi/deserializer/slots.py | 112 ++++++++++++++++++ src/plone/restapi/events.py | 10 ++ src/plone/restapi/interfaces.py | 6 + src/plone/restapi/services/slots/update.py | 6 + src/plone/restapi/slots.py | 2 +- 6 files changed, 180 insertions(+), 22 deletions(-) create mode 100644 src/plone/restapi/deserializer/slots.py create mode 100644 src/plone/restapi/events.py diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml index c91a266282..6bca659257 100644 --- a/src/plone/restapi/deserializer/configure.zcml +++ b/src/plone/restapi/deserializer/configure.zcml @@ -1,7 +1,8 @@ + i18n_domain="plone.restapi" + > @@ -19,25 +20,48 @@ - - - - - - - - - - + + + + + + + + + + + + + + + @@ -46,7 +70,7 @@ - + diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py new file mode 100644 index 0000000000..a4922245cd --- /dev/null +++ b/src/plone/restapi/deserializer/slots.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +""" Slots deserializers """ + +from plone.restapi.deserializer import json_body +from plone.restapi.events import SlotRemovedEvent +from plone.restapi.interfaces import IBlockFieldDeserializationTransformer +from plone.restapi.interfaces import IDeserializeFromJson +from plone.restapi.interfaces import ISlot +from plone.restapi.interfaces import ISlots +from plone.restapi.slots import Slot +from Products.CMFPlone.interfaces import IContentish +from Products.CMFPlone.interfaces import IPloneSiteRoot +from zope.component import adapter +from zope.component import getMultiAdapter +from zope.component import subscribers +from zope.event import notify +from zope.interface import implementer +from zope.publisher.interfaces.browser import IBrowserRequest + +import copy + + +@adapter(IContentish, ISlot, IBrowserRequest) +@implementer(IDeserializeFromJson) +class SlotDeserializer(object): + """ Deserializer of one slot for contentish objects """ + + def __init__(self, context, slot, request): + self.context = context + self.slot = slot + self.request = request + + def __call__(self, data=None): + if data is None: + data = json_body(self.request) + + slot_blocks = copy.deepcopy(data['slot_blocks']) + + for id, block_value in slot_blocks.items(): + block_type = block_value.get("@type", "") + + handlers = [] + for h in subscribers( + (self.context, self.request), + IBlockFieldDeserializationTransformer, + ): + if h.block_type == block_type or h.block_type is None: + handlers.append(h) + + for handler in sorted(handlers, key=lambda h: h.order): + if not getattr(handler, "disabled", False): + block_value = handler(block_value) + + slot_blocks[id] = block_value + + data['slot_blocks'] = slot_blocks + + for k, v in data.items(): + self.slot[k] = v + + self.slot._p_changed = True + + +@adapter(IPloneSiteRoot, ISlot, IBrowserRequest) +@implementer(IDeserializeFromJson) +class SlotDeserializerRoot(SlotDeserializer): + """ Deserializer of one slot for site root """ + + +@adapter(IContentish, ISlots, IBrowserRequest) +@implementer(IBlockFieldDeserializationTransformer) +class SlotsDeserializer(object): + """ Default deserializer of slots + """ + + def __init__(self, context, storage, request): + self.context = context + self.storage = storage + self.request = request + + def __call__(self, data=None): + if data is None: + data = json_body(self.request) + + for name, slot in self.storage.items(): + slotdata = data.get(name, None) + if slotdata is None: + info = { + 'slot': slot, + 'obj': self.context + } + notify(SlotRemovedEvent(info)) + del self.storage[name] + + for name, slotdata in data.items(): + if name not in self.storage: + slot = Slot() + self.storage[name] = slot + else: + slot = self.storage[name] + + deserializer = getMultiAdapter( + (self.context, slot, self.request), IDeserializeFromJson + ) + deserializer() + + +@adapter(IPloneSiteRoot, ISlots, IBrowserRequest) +@implementer(IDeserializeFromJson) +class SlotsDeserializerRoot(SlotsDeserializer): + """ Deserializer of slots for site root """ diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py new file mode 100644 index 0000000000..76f7537851 --- /dev/null +++ b/src/plone/restapi/events.py @@ -0,0 +1,10 @@ +from plone.restapi.interfaces import ISlotRemovedEvent +from zope.event import Event +from zope.interface import implements + + +class SlotRemovedEvent(Event): + implements(ISlotRemovedEvent) + + def __init__(self, info): + self.info = info diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 2081c25b74..65fc003276 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -5,6 +5,7 @@ # W0221: Arguments number differs from overridden '__call__' method from plone.schema import JSONField +from zope.event.interfaces import IEvent from zope.interface import Attribute from zope.interface import Interface from zope.publisher.interfaces.browser import IDefaultBrowserLayer @@ -247,3 +248,8 @@ class ISlot(Interface): default={"items": []}, required=False, ) + + +class ISlotRemovedEvent(IEvent): + """ A slot has been removed + """ diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index 72726b28b5..64d75004a2 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -10,6 +10,8 @@ from plone.restapi.services import Service from plone.restapi.services.locking.locking import is_locked from zope.component import getMultiAdapter +from zope.event import notify +from zope.lifecycleevent import ObjectModifiedEvent class SlotsPatch(Service): @@ -43,6 +45,8 @@ def reply(self): self.request.response.setStatus(400) return dict(error=dict(type="DeserializationError", message=str(e))) + notify(ObjectModifiedEvent(self.context)) + prefer = self.request.getHeader("Prefer") if prefer == "return=representation": self.request.response.setStatus(200) @@ -70,6 +74,8 @@ def replySlot(self): self.request.response.setStatus(400) return dict(error=dict(type="DeserializationError", message=str(e))) + notify(ObjectModifiedEvent(self.context)) + prefer = self.request.getHeader("Prefer") if prefer == "return=representation": self.request.response.setStatus(200) diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index de639f25f6..b12a5426d8 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -26,5 +26,5 @@ class Slot(Persistent): """A container for data pertaining to a single slot""" def __init__(self): - self.slot_blocks_layout = {} + self.slot_blocks_layout = {"items": []} self.slot_blocks = {} From 3bf47f6692e8b7e97be0e9eba1dcf95b0134de78 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 4 Feb 2021 23:00:33 +0200 Subject: [PATCH 05/99] WIP on deserializers --- src/plone/restapi/events.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py index 76f7537851..e5d940529d 100644 --- a/src/plone/restapi/events.py +++ b/src/plone/restapi/events.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from plone.restapi.interfaces import ISlotRemovedEvent from zope.event import Event from zope.interface import implements From f17d4d1e912b94b3d3a5bf659fc3e790cf006879 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 5 Feb 2021 12:59:59 +0200 Subject: [PATCH 06/99] WIP --- src/plone/restapi/configure.zcml | 9 +++ src/plone/restapi/deserializer/configure.zcml | 11 +-- src/plone/restapi/deserializer/slots.py | 21 ++++-- src/plone/restapi/events.py | 69 +++++++++++++++++-- src/plone/restapi/interfaces.py | 11 ++- .../restapi/profiles/default/catalog.xml | 8 +++ src/plone/restapi/slots.py | 8 ++- .../upgrades/profiles/0007/rolemap.xml | 1 - 8 files changed, 114 insertions(+), 24 deletions(-) create mode 100644 src/plone/restapi/profiles/default/catalog.xml diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index 5757ba550a..a1ce422775 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -126,4 +126,13 @@ for="Products.CMFPlone.interfaces.IPloneSiteRoot" /> + + + diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml index 6bca659257..e5bbb56995 100644 --- a/src/plone/restapi/deserializer/configure.zcml +++ b/src/plone/restapi/deserializer/configure.zcml @@ -20,10 +20,13 @@ - - - - + + + + + + + + + + + diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index b12a5426d8..2b09b4747e 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -2,10 +2,11 @@ from .interfaces import ISlot from .interfaces import ISlots from persistent import Persistent -from persistent.mapping import PersistentMapping from Products.CMFCore.interfaces import IContentish from zope.annotation.factory import factory from zope.component import adapter +from zope.container.btree import BTreeContainer +from zope.container.contained import Contained from zope.interface import implementer @@ -14,7 +15,7 @@ @adapter(IContentish) @implementer(ISlots) -class PersistentSlots(PersistentMapping): +class PersistentSlots(BTreeContainer): """Slots store""" @@ -22,9 +23,10 @@ class PersistentSlots(PersistentMapping): @implementer(ISlot) -class Slot(Persistent): +class Slot(Contained, Persistent): """A container for data pertaining to a single slot""" def __init__(self): + super(Slot, self).__init__() self.slot_blocks_layout = {"items": []} self.slot_blocks = {} diff --git a/src/plone/restapi/upgrades/profiles/0007/rolemap.xml b/src/plone/restapi/upgrades/profiles/0007/rolemap.xml index ab702a8638..b8bf78364b 100644 --- a/src/plone/restapi/upgrades/profiles/0007/rolemap.xml +++ b/src/plone/restapi/upgrades/profiles/0007/rolemap.xml @@ -5,4 +5,3 @@ - From 98e009d59c527362ba6bc4cbda174739edca7ef2 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 6 Feb 2021 21:11:55 +0200 Subject: [PATCH 07/99] Add slots engine unittest --- src/plone/restapi/serializer/slots.py | 13 +- src/plone/restapi/tests/test_slots.py | 283 ++++++++++++++++++++++++++ 2 files changed, 292 insertions(+), 4 deletions(-) create mode 100644 src/plone/restapi/tests/test_slots.py diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index c073c2eed4..05bf4f4410 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -9,6 +9,7 @@ from zope.component import subscribers from zope.interface import implementer from zope.interface import Interface +from zope.publisher.interfaces.browser import IBrowserRequest import copy @@ -16,7 +17,7 @@ SERVICE_ID = "@slots" -@adapter(Interface, ISlot, Interface) +@adapter(Interface, ISlot, IBrowserRequest) @implementer(ISerializeToJson) class SlotSerializer(object): """Default serializer for a single persistent slot""" @@ -52,19 +53,23 @@ def __call__(self, name): } -@adapter(Interface, ISlots, Interface) +@adapter(Interface, ISlots, IBrowserRequest) @implementer(ISerializeToJson) class SlotsSerializer(object): """Default slots storage serializer""" def __call__(self): - result = [] + base_url = self.context.absolute_url() + result = { + '@id': '{}/{}'.format(base_url, SERVICE_ID), + "items": {} + } storage = ISlots(self.context) for name, slot in storage.items(): serializer = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) - result.append(serializer(name)) + result['items'][name] = serializer(name) return result diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py new file mode 100644 index 0000000000..7affbe79be --- /dev/null +++ b/src/plone/restapi/tests/test_slots.py @@ -0,0 +1,283 @@ +from copy import deepcopy +from six.moves import UserDict + +import unittest + + +_missing = object() + + +class SlotsEngine(object): + def __init__(self, context): + self.context = context + + def get_slots_stack(self, name): + slot_stack = [] + + current = self.context + while True: + slot = current.slots.get(name) + if slot: + slot_stack.append(slot) + if current.__parent__: + current = current.__parent__ + else: + break + + return slot_stack + + def get_slots(self, name): + blocks = {} + blocks_layout = [] + + _override = set() + + stack = self.get_slots_stack(name) + + level = 0 + for slot in stack: + for uid, block in slot['slot_blocks'].items(): + block = deepcopy(block) + + if not (uid in blocks or uid in _override): + if block.get('_override'): + _override.add(block['_override']) + + blocks[uid] = block + if level > 0: + block['_v_inherit'] = True + + for uid in slot['slot_blocks_layout']['items']: + if uid not in blocks_layout and uid not in _override: + blocks_layout.append(uid) + + level += 1 + + return { + 'slot_blocks': blocks, + 'slot_blocks_layout': {'items': blocks_layout} + } + + +class Content(object): + __name__ = None + __parent__ = None + + def __init__(self, data=None): + self.children = {} + self.slots = {} + + self.data = data + + def __getitem__(self, key): + return self.children[key] + + def __setitem__(self, name, child): + child.__name__ = name + child.__parent__ = self + self.children[name] = child + + def get(self, name, default=_missing): + if name in self.children: + return self.children[name] + + if default != _missing: + return default + + @property + def parent(self): + return self.__parent__ + + def __repr__(self): + stack = [] + current = self + + while True: + if (current.__name__): + stack.append(current.__name__) + if current.__parent__: + current = current.__parent__ + else: + break + + return "" .format("/".join(reversed(stack))) + + +class Slots(Content): + pass + + +class Slot(UserDict): + + @classmethod + def from_data(cls, blocks, layout): + res = { + 'slot_blocks_layout': {"items": layout}, + 'slot_blocks': blocks + } + + return cls(res) + + +class TestSlots(unittest.TestCase): + + def make_content(self): + + root = Content() + root['documents'] = Content() + root['documents']['internal'] = Content() + root['documents']['internal']['company-a'] = Content() + root['documents']['internal']['company-a']['doc-1'] = Content() + root['documents']['internal']['company-a']['doc-2'] = Content() + root['documents']['internal']['company-a']['doc-3'] = Content() + + root['documents']['external'] = Content() + root['documents']['external']['company-a'] = Content() + root['documents']['external']['company-a']['doc-1'] = Content() + root['documents']['external']['company-a']['doc-2'] = Content() + root['documents']['external']['company-a']['doc-3'] = Content() + + root['images'] = Content() + + root.slots = Slots() + root.slots['left'] = Slot( + { + 'slot_blocks': { + 1: {}, + 2: {}, + 3: {}, + }, + 'slot_blocks_layout': { + 'items': [1, 2, 3] + } + } + ) + root.slots['right'] = Slot() + + return root + + def test_slot_stack_on_root(self): + root = self.make_content() + engine = SlotsEngine(root) + + self.assertEqual(engine.get_slots_stack('bottom'), []) + self.assertEqual(engine.get_slots_stack('right'), []) + self.assertEqual(engine.get_slots_stack('left'), [ + {'slot_blocks_layout': {'items': [1, 2, 3]}, + 'slot_blocks': { + 1: {}, 2: {}, 3: {} + }} + ]) + + def test_slot_stack_deep(self): + root = self.make_content() + engine = SlotsEngine(root['documents']['internal']['company-a']) + + self.assertEqual(engine.get_slots_stack('bottom'), []) + self.assertEqual(engine.get_slots_stack('right'), []) + self.assertEqual(engine.get_slots_stack('left'), [ + {'slot_blocks_layout': {'items': [1, 2, 3]}, + 'slot_blocks': { + 1: {}, 2: {}, 3: {} + }} + ]) + + def test_slot_stack_deep_with_data_in_root(self): + root = self.make_content() + obj = root['documents']['internal']['company-a'] + + slot = Slot.from_data({ + 4: {}, 5: {}, 6: {} + }, [4, 5, 6]) + + obj.slots['left'] = slot + engine = SlotsEngine(obj) + + self.assertEqual(engine.get_slots_stack('left'), [ + { + 'slot_blocks_layout': {'items': [4, 5, 6]}, + 'slot_blocks': {4: {}, 5: {}, 6: {}}}, + { + 'slot_blocks_layout': {'items': [1, 2, 3]}, + 'slot_blocks': {1: {}, 2: {}, 3: {}}} + ]) + + def test_slot_stack_deep_with_stack_collapse(self): + root = self.make_content() + + root['documents'].slots['left'] = Slot.from_data({ + 4: {}, 5: {}, 6: {} + }, [4, 5, 6]) + + obj = root['documents']['internal']['company-a'] + obj.slots['left'] = Slot.from_data({ + 4: {}, 5: {}, 6: {}, 7: {} + }, [4, 5, 6, 7]) + + engine = SlotsEngine(obj) + + left = engine.get_slots('left') + self.assertEqual(left, { + 'slot_blocks_layout': {'items': [4, 5, 6, 7, 1, 2, 3]}, + 'slot_blocks': { + 4: {}, + 5: {}, + 6: {}, + 1: {'_v_inherit': True, }, + 2: {'_v_inherit': True, }, + 3: {'_v_inherit': True, }, + 7: {} + } + }) + + def test_block_data_gets_inherited(self): + root = self.make_content() + root.slots = {} + + root['documents'].slots['left'] = Slot.from_data({ + 1: {'title': 'First'} + }, [1]) + + obj = root['documents']['internal'] + obj.slots['left'] = Slot.from_data({2: {}}, [2]) + + engine = SlotsEngine(obj) + left = engine.get_slots('left') + + self.assertEqual(left, { + 'slot_blocks_layout': {'items': [ + 2, 1 + ]}, + 'slot_blocks': { + 1: {'title': 'First', '_v_inherit': True}, + 2: {} + } + }) + + def test_block_data_gets_override(self): + root = self.make_content() + root.slots = {} + + root['documents'].slots['left'] = Slot.from_data({ + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + }, [1, 3]) + + obj = root['documents']['internal'] + obj.slots['left'] = Slot.from_data({2: { + '_override': 1, + 'title': 'Second' + }}, [2]) + + engine = SlotsEngine(obj) + left = engine.get_slots('left') + + self.assertEqual(left, { + 'slot_blocks_layout': {'items': [ + 2, 3 + ]}, + 'slot_blocks': { + 2: {'title': 'Second', '_override': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + } + }) From 34bc973fe323e96ebef78d94d9be8097bcafcb55 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 6 Feb 2021 21:43:11 +0200 Subject: [PATCH 08/99] Improve test formatting --- src/plone/restapi/tests/test_slots.py | 112 +++++++++++++++----------- 1 file changed, 65 insertions(+), 47 deletions(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 7affbe79be..e4fa5777f0 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -103,10 +103,6 @@ def __repr__(self): return "" .format("/".join(reversed(stack))) -class Slots(Content): - pass - - class Slot(UserDict): @classmethod @@ -139,56 +135,54 @@ def make_content(self): root['images'] = Content() - root.slots = Slots() - root.slots['left'] = Slot( - { - 'slot_blocks': { - 1: {}, - 2: {}, - 3: {}, - }, - 'slot_blocks_layout': { - 'items': [1, 2, 3] - } - } - ) - root.slots['right'] = Slot() - return root def test_slot_stack_on_root(self): + # simple test with one level stack of slots root = self.make_content() + root.slots['left'] = Slot({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + }) + root.slots['right'] = Slot() engine = SlotsEngine(root) self.assertEqual(engine.get_slots_stack('bottom'), []) self.assertEqual(engine.get_slots_stack('right'), []) self.assertEqual(engine.get_slots_stack('left'), [ - {'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': { - 1: {}, 2: {}, 3: {} - }} + { + 'slot_blocks_layout': {'items': [1, 2, 3]}, + 'slot_blocks': {1: {}, 2: {}, 3: {}} + } ]) def test_slot_stack_deep(self): + # the slot stack is inherited further down root = self.make_content() + root.slots['left'] = Slot({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + }) engine = SlotsEngine(root['documents']['internal']['company-a']) self.assertEqual(engine.get_slots_stack('bottom'), []) self.assertEqual(engine.get_slots_stack('right'), []) self.assertEqual(engine.get_slots_stack('left'), [ {'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': { - 1: {}, 2: {}, 3: {} - }} + 'slot_blocks': {1: {}, 2: {}, 3: {}}} ]) def test_slot_stack_deep_with_data_in_root(self): + # slots stacks up from deepest to shallow root = self.make_content() + root.slots['left'] = Slot({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + }) obj = root['documents']['internal']['company-a'] - slot = Slot.from_data({ - 4: {}, 5: {}, 6: {} - }, [4, 5, 6]) + slot = Slot.from_data({4: {}, 5: {}, 6: {}}, + [4, 5, 6]) obj.slots['left'] = slot engine = SlotsEngine(obj) @@ -204,15 +198,17 @@ def test_slot_stack_deep_with_data_in_root(self): def test_slot_stack_deep_with_stack_collapse(self): root = self.make_content() + root.slots['left'] = Slot({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + }) - root['documents'].slots['left'] = Slot.from_data({ - 4: {}, 5: {}, 6: {} - }, [4, 5, 6]) + root['documents'].slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}}, + [4, 5, 6]) obj = root['documents']['internal']['company-a'] - obj.slots['left'] = Slot.from_data({ - 4: {}, 5: {}, 6: {}, 7: {} - }, [4, 5, 6, 7]) + obj.slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, + [4, 5, 6, 7]) engine = SlotsEngine(obj) @@ -232,7 +228,6 @@ def test_slot_stack_deep_with_stack_collapse(self): def test_block_data_gets_inherited(self): root = self.make_content() - root.slots = {} root['documents'].slots['left'] = Slot.from_data({ 1: {'title': 'First'} @@ -245,9 +240,7 @@ def test_block_data_gets_inherited(self): left = engine.get_slots('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [ - 2, 1 - ]}, + 'slot_blocks_layout': {'items': [2, 1]}, 'slot_blocks': { 1: {'title': 'First', '_v_inherit': True}, 2: {} @@ -256,7 +249,6 @@ def test_block_data_gets_inherited(self): def test_block_data_gets_override(self): root = self.make_content() - root.slots = {} root['documents'].slots['left'] = Slot.from_data({ 1: {'title': 'First'}, @@ -264,20 +256,46 @@ def test_block_data_gets_override(self): }, [1, 3]) obj = root['documents']['internal'] - obj.slots['left'] = Slot.from_data({2: { - '_override': 1, - 'title': 'Second' - }}, [2]) + obj.slots['left'] = Slot.from_data({ + 2: {'_override': 1, 'title': 'Second'}}, + [2]) engine = SlotsEngine(obj) left = engine.get_slots('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [ - 2, 3 - ]}, + 'slot_blocks_layout': {'items': [2, 3]}, 'slot_blocks': { 2: {'title': 'Second', '_override': 1}, 3: {'title': 'Third', '_v_inherit': True}, } }) + + # def test_block_data_gets_override_complex(self): + # root = self.make_content() + # + # root.slots['left'] = Slot.from_data({ + # 1: {'title': 'First'}, + # 3: {'title': 'Third'}, + # }, [1, 3]) + # + # root['documents'].slots['left'] = Slot.from_data({ + # 2: {'title': 'Second'}, + # 4: {'_override': 3}, + # }, [2, 4]) + # + # obj = root['documents']['internal'] + # obj.slots['left'] = Slot.from_data({ + # 2: {'_override': 1, 'title': 'Second'}}, + # [2]) + # + # engine = SlotsEngine(obj) + # left = engine.get_slots('left') + # + # self.assertEqual(left, { + # 'slot_blocks_layout': {'items': [2, 3]}, + # 'slot_blocks': { + # 2: {'title': 'Second', '_override': 1}, + # 3: {'title': 'Third', '_v_inherit': True}, + # } + # }) From 02223e0a450e6759a469f46ce7fb1768a373a526 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 6 Feb 2021 23:49:30 +0200 Subject: [PATCH 09/99] More tests on slot engine crutch --- src/plone/restapi/serializer/configure.zcml | 8 ++++-- src/plone/restapi/tests/test_slots.py | 31 +++++++-------------- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml index b24e5eb332..c1a1c4474a 100644 --- a/src/plone/restapi/serializer/configure.zcml +++ b/src/plone/restapi/serializer/configure.zcml @@ -44,7 +44,12 @@ provides="plone.restapi.interfaces.IBlockFieldSerializationTransformer" /> - + + @@ -117,5 +122,4 @@ - diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index e4fa5777f0..0f502f24bc 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from copy import deepcopy from six.moves import UserDict @@ -77,17 +79,6 @@ def __setitem__(self, name, child): child.__parent__ = self self.children[name] = child - def get(self, name, default=_missing): - if name in self.children: - return self.children[name] - - if default != _missing: - return default - - @property - def parent(self): - return self.__parent__ - def __repr__(self): stack = [] current = self @@ -197,16 +188,16 @@ def test_slot_stack_deep_with_data_in_root(self): ]) def test_slot_stack_deep_with_stack_collapse(self): + # get_slots collapses the stack and marks inherited slots with _v_inherit + root = self.make_content() - root.slots['left'] = Slot({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} - }) + obj = root['documents']['internal']['company-a'] + + root.slots['left'] = Slot.from_data({1: {}, 2: {}, 3: {}, }, [1, 2, 3]) root['documents'].slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}}, [4, 5, 6]) - obj = root['documents']['internal']['company-a'] obj.slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, [4, 5, 6, 7]) @@ -227,13 +218,11 @@ def test_slot_stack_deep_with_stack_collapse(self): }) def test_block_data_gets_inherited(self): + # blocks that are inherited from parents are marked with _v_inherit root = self.make_content() - - root['documents'].slots['left'] = Slot.from_data({ - 1: {'title': 'First'} - }, [1]) - obj = root['documents']['internal'] + + root['documents'].slots['left'] = Slot.from_data({1: {'title': 'First'}}, [1]) obj.slots['left'] = Slot.from_data({2: {}}, [2]) engine = SlotsEngine(obj) From 68d534cd1451c3e08beb2a0368c268ffb7d3fd99 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 6 Feb 2021 23:55:48 +0200 Subject: [PATCH 10/99] Add VolatileSmartField --- src/plone/restapi/deserializer/blocks.py | 21 ++++++++++++++++++- src/plone/restapi/deserializer/configure.zcml | 4 ++++ src/plone/restapi/tests/test_slots.py | 1 + 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index b5724766eb..ac9b0abb38 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -35,7 +35,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1 :] + path = path[len(portal_url) + 1:] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") @@ -180,6 +180,25 @@ def __call__(self, block): return block +class VolatileSmartField(object): + """ When deserializing block values, delete all block fields that start with `_v_` + """ + + order = float('inf') + block_type = None + + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, block): + for k, v in block.items(): + if k.startswith('_v_'): + del v[k] + + return block + + @adapter(IBlocks, IBrowserRequest) @implementer(IBlockFieldDeserializationTransformer) class ResolveUIDDeserializer(ResolveUIDDeserializerBase): diff --git a/src/plone/restapi/deserializer/configure.zcml b/src/plone/restapi/deserializer/configure.zcml index ad4a521431..6a8e9a51b4 100644 --- a/src/plone/restapi/deserializer/configure.zcml +++ b/src/plone/restapi/deserializer/configure.zcml @@ -60,6 +60,10 @@ factory=".blocks.ResolveUIDDeserializerRoot" provides="plone.restapi.interfaces.IBlockFieldDeserializationTransformer" /> + Date: Sun, 7 Feb 2021 00:47:09 +0200 Subject: [PATCH 11/99] WIP --- src/plone/restapi/tests/test_slots.py | 89 ++++++++++++++++----------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 03694e0173..509c285a09 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -32,7 +32,8 @@ def get_slots(self, name): blocks = {} blocks_layout = [] - _override = set() + _replaced = set() + _blockmap = {} stack = self.get_slots_stack(name) @@ -40,26 +41,41 @@ def get_slots(self, name): for slot in stack: for uid, block in slot['slot_blocks'].items(): block = deepcopy(block) + _blockmap[uid] = block - if not (uid in blocks or uid in _override): - if block.get('_override'): - _override.add(block['_override']) + if not (uid in blocks or uid in _replaced): + other = block.get('s:isVariantOf') or block.get('s:sameAs') + if other: + _replaced.add(other) blocks[uid] = block if level > 0: block['_v_inherit'] = True for uid in slot['slot_blocks_layout']['items']: - if uid not in blocks_layout and uid not in _override: + if not (uid in blocks_layout or uid in _replaced): blocks_layout.append(uid) level += 1 + for k, v in blocks.items(): + if v.get('s:sameAs'): + v['_v_inherit'] = True + v.update(self._resolve_block(v, _blockmap)) + return { 'slot_blocks': blocks, 'slot_blocks_layout': {'items': blocks_layout} } + def _resolve_block(self, block, blocks): + sameAs = block.get('s:sameAs') + + if sameAs: + return self._resolve_block(blocks[sameAs], blocks) + + return block + class Content(object): __name__ = None @@ -238,6 +254,8 @@ def test_block_data_gets_inherited(self): }) def test_block_data_gets_override(self): + # a child can override the data for a parent block, in a new block + root = self.make_content() root['documents'].slots['left'] = Slot.from_data({ @@ -247,7 +265,7 @@ def test_block_data_gets_override(self): obj = root['documents']['internal'] obj.slots['left'] = Slot.from_data({ - 2: {'_override': 1, 'title': 'Second'}}, + 2: {'s:isVariantOf': 1, 'title': 'Second'}}, [2]) engine = SlotsEngine(obj) @@ -256,36 +274,37 @@ def test_block_data_gets_override(self): self.assertEqual(left, { 'slot_blocks_layout': {'items': [2, 3]}, 'slot_blocks': { - 2: {'title': 'Second', '_override': 1}, + 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, } }) - # def test_block_data_gets_override_complex(self): - # root = self.make_content() - # - # root.slots['left'] = Slot.from_data({ - # 1: {'title': 'First'}, - # 3: {'title': 'Third'}, - # }, [1, 3]) - # - # root['documents'].slots['left'] = Slot.from_data({ - # 2: {'title': 'Second'}, - # 4: {'_override': 3}, - # }, [2, 4]) - # - # obj = root['documents']['internal'] - # obj.slots['left'] = Slot.from_data({ - # 2: {'_override': 1, 'title': 'Second'}}, - # [2]) - # - # engine = SlotsEngine(obj) - # left = engine.get_slots('left') - # - # self.assertEqual(left, { - # 'slot_blocks_layout': {'items': [2, 3]}, - # 'slot_blocks': { - # 2: {'title': 'Second', '_override': 1}, - # 3: {'title': 'Third', '_v_inherit': True}, - # } - # }) + def test_can_change_order(self): + # a child can change the order inherited from its parents + # to reposition a parent, + + root = self.make_content() + + root['documents'].slots['left'] = Slot.from_data({ + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, [5, 1, 3]) + + obj = root['documents']['internal'] + obj.slots['left'] = Slot.from_data({ + 2: {'s:isVariantOf': 1, 'title': 'Second'}, + 4: {'s:sameAs': 3} + }, [4, 2]) + + engine = SlotsEngine(obj) + left = engine.get_slots('left') + + self.assertEqual(left, { + 'slot_blocks_layout': {'items': [4, 2, 5]}, + 'slot_blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 4: {'title': 'Third', 's:sameAs': 3, '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + } + }) From 09630a27a9b8a2f66aa3ce801a07e5adc13d6e57 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 7 Feb 2021 08:51:33 +0200 Subject: [PATCH 12/99] Add another way of treating reordered slot fills --- src/plone/restapi/tests/test_slots.py | 37 ++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 509c285a09..8ef9ec0e9e 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -134,12 +134,6 @@ def make_content(self): root['documents']['internal']['company-a']['doc-2'] = Content() root['documents']['internal']['company-a']['doc-3'] = Content() - root['documents']['external'] = Content() - root['documents']['external']['company-a'] = Content() - root['documents']['external']['company-a']['doc-1'] = Content() - root['documents']['external']['company-a']['doc-2'] = Content() - root['documents']['external']['company-a']['doc-3'] = Content() - root['images'] = Content() return root @@ -279,7 +273,7 @@ def test_block_data_gets_override(self): } }) - def test_can_change_order(self): + def test_can_change_order_with_sameOf(self): # a child can change the order inherited from its parents # to reposition a parent, @@ -308,3 +302,32 @@ def test_can_change_order(self): 5: {'title': 'Fifth', '_v_inherit': True}, } }) + + def test_can_change_order_from_layout(self): + # a child can change the order inherited from parents by simply repositioning + # the parent id in their layout + + root = self.make_content() + + root['documents'].slots['left'] = Slot.from_data({ + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, [5, 1, 3]) + + obj = root['documents']['internal'] + obj.slots['left'] = Slot.from_data({ + 2: {'s:isVariantOf': 1, 'title': 'Second'}, + }, [3, 2]) + + engine = SlotsEngine(obj) + left = engine.get_slots('left') + + self.assertEqual(left, { + 'slot_blocks_layout': {'items': [3, 2, 5]}, + 'slot_blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + } + }) From 48f3865e66e1655bd1aaf9ffd5d9d268a5926456 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 7 Feb 2021 09:33:16 +0200 Subject: [PATCH 13/99] Add save_data_to_slot to slot engine --- src/plone/restapi/tests/test_slots.py | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 8ef9ec0e9e..37713b6342 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -76,6 +76,22 @@ def _resolve_block(self, block, blocks): return block + def save_data_to_slot(self, store, data): + to_save = {} + for key in data['slot_blocks_layout']['items']: + block = data['slot_blocks'][key] + if not (block.get('s:sameOf') or block.get('_v_inherit')): + to_save[key] = block + + for k, v in data.items(): + if k not in ['slot_blocks_layout', 'slot_blocks']: + store[k] = v + + store['slot_blocks_layout'] = data['slot_blocks_layout'] + store['slot_blocks'] = to_save + + return store + class Content(object): __name__ = None @@ -331,3 +347,26 @@ def test_can_change_order_from_layout(self): 5: {'title': 'Fifth', '_v_inherit': True}, } }) + + def test_save_slots(self): + data = { + 'slot_blocks_layout': {'items': [3, 2, 5]}, + 'slot_blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + }, + 'extra': 'data', + } + + root = self.make_content() + obj = root['documents']['internal'] + engine = SlotsEngine(obj) + + slot = {} + engine.save_data_to_slot(slot, data) + + self.assertEqual(slot, { + 'extra': 'data', + 'slot_blocks': {2: {'s:isVariantOf': 1, 'title': 'Second'}}, + 'slot_blocks_layout': {'items': [3, 2, 5]}}) From ef71439ecf5d52fdd0a39052707c014256cb642f Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 7 Feb 2021 12:51:16 +0200 Subject: [PATCH 14/99] Move SlotsEngine to .slots, tweak names --- src/plone/restapi/configure.zcml | 8 +- src/plone/restapi/interfaces.py | 4 + src/plone/restapi/slots.py | 105 +++++++++++++++++- src/plone/restapi/tests/test_slots.py | 149 +++++++------------------- 4 files changed, 147 insertions(+), 119 deletions(-) diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index a1ce422775..8aa4207699 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -115,14 +115,14 @@ /> diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 327ed742bc..9b7eed1590 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -220,6 +220,10 @@ class ISlots(Interface): """Slots are named container of sets of blocks""" +class ISlotsStorage(Interface): + """ A store of slots information """ + + SLOT_BLOCKS_SCHEMA = json.dumps({"type": "object", "properties": {}}) SLOT_LAYOUT_SCHEMA = json.dumps( diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 2b09b4747e..7c2e4a57c7 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- + from .interfaces import ISlot from .interfaces import ISlots +from .interfaces import ISlotsStorage +from copy import deepcopy from persistent import Persistent from Products.CMFCore.interfaces import IContentish from zope.annotation.factory import factory @@ -8,18 +11,24 @@ from zope.container.btree import BTreeContainer from zope.container.contained import Contained from zope.interface import implementer +from zope.publisher.interfaces.browser import IBrowserRequest +from zope.traversing.interfaces import ITraversable SLOTS_KEY = "plone.restapi.slots" +DEFAULT_SLOT_DATA = { + "slot_blocks_layout": {"items": []}, + "slot_blocks": {} +} @adapter(IContentish) -@implementer(ISlots) +@implementer(ISlotsStorage) class PersistentSlots(BTreeContainer): - """Slots store""" + """ Slots container""" -Slots = factory(PersistentSlots, SLOTS_KEY) +SlotsStorage = factory(PersistentSlots, SLOTS_KEY) @implementer(ISlot) @@ -28,5 +37,91 @@ class Slot(Contained, Persistent): def __init__(self): super(Slot, self).__init__() - self.slot_blocks_layout = {"items": []} - self.slot_blocks = {} + + for k, v in deepcopy(DEFAULT_SLOT_DATA).items(): + setattr(self, k, v) + + +@implementer(ISlots) +@adapter(ITraversable, IBrowserRequest) +class Slots(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def get_fills_stack(self, name): + slot_stack = [] + + current = self.context + while True: + slot = ISlotsStorage(current).get(name) + if slot: + slot_stack.append(slot) + if current.__parent__: + current = current.__parent__ + else: + break + + return slot_stack + + def get_blocks(self, name): + blocks = {} + blocks_layout = [] + + _replaced = set() + _blockmap = {} + + stack = self.get_fills_stack(name) + + level = 0 + for slot in stack: + for uid, block in slot['slot_blocks'].items(): + block = deepcopy(block) + _blockmap[uid] = block + + if not (uid in blocks or uid in _replaced): + other = block.get('s:isVariantOf') or block.get('s:sameAs') + if other: + _replaced.add(other) + + blocks[uid] = block + if level > 0: + block['_v_inherit'] = True + + for uid in slot['slot_blocks_layout']['items']: + if not (uid in blocks_layout or uid in _replaced): + blocks_layout.append(uid) + + level += 1 + + for k, v in blocks.items(): + if v.get('s:sameAs'): + v['_v_inherit'] = True + v.update(self._resolve_block(v, _blockmap)) + + return { + 'slot_blocks': blocks, + 'slot_blocks_layout': {'items': blocks_layout} + } + + def _resolve_block(self, block, blocks): + sameAs = block.get('s:sameAs') + + if sameAs: + return self._resolve_block(blocks[sameAs], blocks) + + return block + + def save_data_to_slot(self, slot, data): + to_save = {} + for key in data['slot_blocks_layout']['items']: + block = data['slot_blocks'][key] + if not (block.get('s:sameOf') or block.get('_v_inherit')): + to_save[key] = block + + for k, v in data.items(): + if k not in ['slot_blocks_layout', 'slot_blocks']: + slot[k] = v + + slot['slot_blocks_layout'] = data['slot_blocks_layout'] + slot['slot_blocks'] = to_save diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 37713b6342..84b9cb4669 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -1,98 +1,15 @@ # -*- coding: utf-8 -*- -from copy import deepcopy +from plone.restapi.interfaces import ISlotsStorage +from plone.restapi.slots import Slots from six.moves import UserDict +from zope.component import provideAdapter +from zope.interface import implements +from zope.interface import Interface import unittest -_missing = object() - - -class SlotsEngine(object): - def __init__(self, context): - self.context = context - - def get_slots_stack(self, name): - slot_stack = [] - - current = self.context - while True: - slot = current.slots.get(name) - if slot: - slot_stack.append(slot) - if current.__parent__: - current = current.__parent__ - else: - break - - return slot_stack - - def get_slots(self, name): - blocks = {} - blocks_layout = [] - - _replaced = set() - _blockmap = {} - - stack = self.get_slots_stack(name) - - level = 0 - for slot in stack: - for uid, block in slot['slot_blocks'].items(): - block = deepcopy(block) - _blockmap[uid] = block - - if not (uid in blocks or uid in _replaced): - other = block.get('s:isVariantOf') or block.get('s:sameAs') - if other: - _replaced.add(other) - - blocks[uid] = block - if level > 0: - block['_v_inherit'] = True - - for uid in slot['slot_blocks_layout']['items']: - if not (uid in blocks_layout or uid in _replaced): - blocks_layout.append(uid) - - level += 1 - - for k, v in blocks.items(): - if v.get('s:sameAs'): - v['_v_inherit'] = True - v.update(self._resolve_block(v, _blockmap)) - - return { - 'slot_blocks': blocks, - 'slot_blocks_layout': {'items': blocks_layout} - } - - def _resolve_block(self, block, blocks): - sameAs = block.get('s:sameAs') - - if sameAs: - return self._resolve_block(blocks[sameAs], blocks) - - return block - - def save_data_to_slot(self, store, data): - to_save = {} - for key in data['slot_blocks_layout']['items']: - block = data['slot_blocks'][key] - if not (block.get('s:sameOf') or block.get('_v_inherit')): - to_save[key] = block - - for k, v in data.items(): - if k not in ['slot_blocks_layout', 'slot_blocks']: - store[k] = v - - store['slot_blocks_layout'] = data['slot_blocks_layout'] - store['slot_blocks'] = to_save - - return store - - class Content(object): __name__ = None __parent__ = None @@ -126,6 +43,16 @@ def __repr__(self): return "" .format("/".join(reversed(stack))) +class SlotsStorage(object): + implements(ISlotsStorage) + + def __init__(self, context): + self.context = context + + def get(self, name): + return self.context.slots.get(name) + + class Slot(UserDict): @classmethod @@ -139,6 +66,8 @@ def from_data(cls, blocks, layout): class TestSlots(unittest.TestCase): + def setUp(self): + provideAdapter(SlotsStorage, [Interface], ISlotsStorage) def make_content(self): @@ -162,11 +91,11 @@ def test_slot_stack_on_root(self): 'slot_blocks_layout': {'items': [1, 2, 3]} }) root.slots['right'] = Slot() - engine = SlotsEngine(root) + engine = Slots(root, None) - self.assertEqual(engine.get_slots_stack('bottom'), []) - self.assertEqual(engine.get_slots_stack('right'), []) - self.assertEqual(engine.get_slots_stack('left'), [ + self.assertEqual(engine.get_fills_stack('bottom'), []) + self.assertEqual(engine.get_fills_stack('right'), []) + self.assertEqual(engine.get_fills_stack('left'), [ { 'slot_blocks_layout': {'items': [1, 2, 3]}, 'slot_blocks': {1: {}, 2: {}, 3: {}} @@ -180,11 +109,11 @@ def test_slot_stack_deep(self): 'slot_blocks': {1: {}, 2: {}, 3: {}, }, 'slot_blocks_layout': {'items': [1, 2, 3]} }) - engine = SlotsEngine(root['documents']['internal']['company-a']) + engine = Slots(root['documents']['internal']['company-a'], None) - self.assertEqual(engine.get_slots_stack('bottom'), []) - self.assertEqual(engine.get_slots_stack('right'), []) - self.assertEqual(engine.get_slots_stack('left'), [ + self.assertEqual(engine.get_fills_stack('bottom'), []) + self.assertEqual(engine.get_fills_stack('right'), []) + self.assertEqual(engine.get_fills_stack('left'), [ {'slot_blocks_layout': {'items': [1, 2, 3]}, 'slot_blocks': {1: {}, 2: {}, 3: {}}} ]) @@ -202,9 +131,9 @@ def test_slot_stack_deep_with_data_in_root(self): [4, 5, 6]) obj.slots['left'] = slot - engine = SlotsEngine(obj) + engine = Slots(obj, None) - self.assertEqual(engine.get_slots_stack('left'), [ + self.assertEqual(engine.get_fills_stack('left'), [ { 'slot_blocks_layout': {'items': [4, 5, 6]}, 'slot_blocks': {4: {}, 5: {}, 6: {}}}, @@ -214,7 +143,7 @@ def test_slot_stack_deep_with_data_in_root(self): ]) def test_slot_stack_deep_with_stack_collapse(self): - # get_slots collapses the stack and marks inherited slots with _v_inherit + # get_blocks collapses the stack and marks inherited slots with _v_inherit root = self.make_content() obj = root['documents']['internal']['company-a'] @@ -227,9 +156,9 @@ def test_slot_stack_deep_with_stack_collapse(self): obj.slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, [4, 5, 6, 7]) - engine = SlotsEngine(obj) + engine = Slots(obj, None) - left = engine.get_slots('left') + left = engine.get_blocks('left') self.assertEqual(left, { 'slot_blocks_layout': {'items': [4, 5, 6, 7, 1, 2, 3]}, 'slot_blocks': { @@ -252,8 +181,8 @@ def test_block_data_gets_inherited(self): root['documents'].slots['left'] = Slot.from_data({1: {'title': 'First'}}, [1]) obj.slots['left'] = Slot.from_data({2: {}}, [2]) - engine = SlotsEngine(obj) - left = engine.get_slots('left') + engine = Slots(obj, None) + left = engine.get_blocks('left') self.assertEqual(left, { 'slot_blocks_layout': {'items': [2, 1]}, @@ -278,8 +207,8 @@ def test_block_data_gets_override(self): 2: {'s:isVariantOf': 1, 'title': 'Second'}}, [2]) - engine = SlotsEngine(obj) - left = engine.get_slots('left') + engine = Slots(obj, None) + left = engine.get_blocks('left') self.assertEqual(left, { 'slot_blocks_layout': {'items': [2, 3]}, @@ -307,8 +236,8 @@ def test_can_change_order_with_sameOf(self): 4: {'s:sameAs': 3} }, [4, 2]) - engine = SlotsEngine(obj) - left = engine.get_slots('left') + engine = Slots(obj, None) + left = engine.get_blocks('left') self.assertEqual(left, { 'slot_blocks_layout': {'items': [4, 2, 5]}, @@ -336,8 +265,8 @@ def test_can_change_order_from_layout(self): 2: {'s:isVariantOf': 1, 'title': 'Second'}, }, [3, 2]) - engine = SlotsEngine(obj) - left = engine.get_slots('left') + engine = Slots(obj, None) + left = engine.get_blocks('left') self.assertEqual(left, { 'slot_blocks_layout': {'items': [3, 2, 5]}, @@ -361,7 +290,7 @@ def test_save_slots(self): root = self.make_content() obj = root['documents']['internal'] - engine = SlotsEngine(obj) + engine = Slots(obj, None) slot = {} engine.save_data_to_slot(slot, data) From 3af5692a4a2dbb07dc715bfb9639edb8bc9b57a1 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 7 Feb 2021 23:28:25 +0200 Subject: [PATCH 15/99] Rename ISlotsStorage -> ISlotStorage --- src/plone/restapi/configure.zcml | 4 ++-- src/plone/restapi/deserializer/blocks.py | 2 ++ src/plone/restapi/deserializer/slots.py | 8 +++++--- src/plone/restapi/interfaces.py | 2 +- src/plone/restapi/serializer/configure.zcml | 1 + src/plone/restapi/serializer/slots.py | 9 ++++++--- src/plone/restapi/services/slots/get.py | 18 +++++++++--------- src/plone/restapi/slots.py | 9 ++++++--- src/plone/restapi/tests/test_slots.py | 6 +++--- 9 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index 8aa4207699..ddb396c568 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -116,13 +116,13 @@ diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index ac9b0abb38..68a6a103a8 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -180,6 +180,8 @@ def __call__(self, block): return block +@adapter(IBlocks, IBrowserRequest) +@implementer(IBlockFieldDeserializationTransformer) class VolatileSmartField(object): """ When deserializing block values, delete all block fields that start with `_v_` """ diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index d054af9bdc..412d14f02c 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -7,7 +7,7 @@ from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.interfaces import ISlot -from plone.restapi.interfaces import ISlots +from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot from Products.CMFCore.interfaces import IContentish from Products.CMFPlone.interfaces import IPloneSiteRoot @@ -36,6 +36,8 @@ def __call__(self, data=None): data = json_body(self.request) slot_blocks = copy.deepcopy(data['slot_blocks']) + + # TODO: use ISlotStorage existing_blocks = self.slot.slot_blocks removed_blocks_ids = set(slot_blocks.keys()) - set(existing_blocks.keys()) @@ -75,7 +77,7 @@ class SlotDeserializerRoot(SlotDeserializer): """ Deserializer of one slot for site root """ -@adapter(IContentish, ISlots, IBrowserRequest) +@adapter(IContentish, ISlotStorage, IBrowserRequest) @implementer(IBlockFieldDeserializationTransformer) class SlotsDeserializer(object): """ Default deserializer of slots @@ -113,7 +115,7 @@ def __call__(self, data=None): deserializer() -@adapter(IPloneSiteRoot, ISlots, IBrowserRequest) +@adapter(IPloneSiteRoot, ISlotStorage, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotsDeserializerRoot(SlotsDeserializer): """ Deserializer of slots for site root """ diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 9b7eed1590..7cd3e2b771 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -220,7 +220,7 @@ class ISlots(Interface): """Slots are named container of sets of blocks""" -class ISlotsStorage(Interface): +class ISlotStorage(Interface): """ A store of slots information """ diff --git a/src/plone/restapi/serializer/configure.zcml b/src/plone/restapi/serializer/configure.zcml index c1a1c4474a..37717d8da7 100644 --- a/src/plone/restapi/serializer/configure.zcml +++ b/src/plone/restapi/serializer/configure.zcml @@ -122,4 +122,5 @@ + diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 05bf4f4410..a810f28f90 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- +# from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISlot -from plone.restapi.interfaces import ISlots +from plone.restapi.interfaces import ISlotStorage from plone.restapi.serializer.converters import json_compatible from zope.component import adapter from zope.component import getMultiAdapter @@ -28,6 +29,8 @@ def __init__(self, context, slot, request): self.slot = slot def __call__(self, name): + + # TODO: use ISlots slot_blocks = copy.deepcopy(self.slot_blocks) slot_blocks_layout = copy.deepcopy(self.slot_blocks_layout) @@ -53,7 +56,7 @@ def __call__(self, name): } -@adapter(Interface, ISlots, IBrowserRequest) +@adapter(Interface, ISlotStorage, IBrowserRequest) @implementer(ISerializeToJson) class SlotsSerializer(object): """Default slots storage serializer""" @@ -64,7 +67,7 @@ def __call__(self): '@id': '{}/{}'.format(base_url, SERVICE_ID), "items": {} } - storage = ISlots(self.context) + storage = ISlotStorage(self.context) for name, slot in storage.items(): serializer = getMultiAdapter( diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index f5f2342896..798faefd62 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.interfaces import ISlots +from plone.restapi.interfaces import ISlotStorage from plone.restapi.services import Service from zope.component import getMultiAdapter from zope.interface import implementer @@ -20,12 +20,10 @@ def publishTraverse(self, request, name): return self def reply(self): - storage = ISlots(self.context) - if self.params and len(self.params) > 0: return self.replySlot() - storage = ISlots(self.context) + storage = ISlotStorage(self.context) adapter = getMultiAdapter( (self.context, storage, self.request), ISerializeToJson @@ -35,16 +33,18 @@ def reply(self): def replySlot(self): name = self.params[0] - storage = ISlots(self.context) + storage = ISlotStorage(self.context) try: slot = storage[name] - return getMultiAdapter( - (self.context, slot, self.request), ISerializeToJson - )(name) except KeyError: self.request.response.setStatus(404) return { "type": "NotFound", - "message": 'Tile "{}" could not be found.'.format(self.params[0]), + "message": 'Slot "{}" could not be found.'.format(self.params[0]), } + finally: + adapter = getMultiAdapter( + (self.context, slot, self.request), ISerializeToJson + ) + return adapter(name) diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 7c2e4a57c7..9e9bbf2673 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -2,7 +2,7 @@ from .interfaces import ISlot from .interfaces import ISlots -from .interfaces import ISlotsStorage +from .interfaces import ISlotStorage from copy import deepcopy from persistent import Persistent from Products.CMFCore.interfaces import IContentish @@ -23,7 +23,7 @@ @adapter(IContentish) -@implementer(ISlotsStorage) +@implementer(ISlotStorage) class PersistentSlots(BTreeContainer): """ Slots container""" @@ -45,6 +45,9 @@ def __init__(self): @implementer(ISlots) @adapter(ITraversable, IBrowserRequest) class Slots(object): + """ The slots engine provides slots functionality for a content item + """ + def __init__(self, context, request): self.context = context self.request = request @@ -54,7 +57,7 @@ def get_fills_stack(self, name): current = self.context while True: - slot = ISlotsStorage(current).get(name) + slot = ISlotStorage(current).get(name) if slot: slot_stack.append(slot) if current.__parent__: diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 84b9cb4669..fb3b5d2318 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from plone.restapi.interfaces import ISlotsStorage +from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slots from six.moves import UserDict from zope.component import provideAdapter @@ -44,7 +44,7 @@ def __repr__(self): class SlotsStorage(object): - implements(ISlotsStorage) + implements(ISlotStorage) def __init__(self, context): self.context = context @@ -67,7 +67,7 @@ def from_data(cls, blocks, layout): class TestSlots(unittest.TestCase): def setUp(self): - provideAdapter(SlotsStorage, [Interface], ISlotsStorage) + provideAdapter(SlotsStorage, [Interface], ISlotStorage) def make_content(self): From 4a479bedef9d896bf707e1f57e467b4c3fde250a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 09:55:01 +0200 Subject: [PATCH 16/99] Move zca registrations to slots.zcml; more tests --- src/plone/restapi/configure.zcml | 22 +---- src/plone/restapi/deserializer/slots.py | 3 +- src/plone/restapi/interfaces.py | 4 +- src/plone/restapi/serializer/slots.py | 18 ++-- src/plone/restapi/slots.py | 28 +++--- src/plone/restapi/slots.zcml | 41 +++++++++ .../restapi/tests/test_serializer_slots.py | 49 +++++++++++ src/plone/restapi/tests/test_slots.py | 86 ++++++++++++++----- 8 files changed, 186 insertions(+), 65 deletions(-) create mode 100644 src/plone/restapi/slots.zcml create mode 100644 src/plone/restapi/tests/test_serializer_slots.py diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index ddb396c568..231fb397ce 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -23,6 +23,7 @@ + - - - - - - - diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 412d14f02c..f2dc8c31d9 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -37,8 +37,7 @@ def __call__(self, data=None): slot_blocks = copy.deepcopy(data['slot_blocks']) - # TODO: use ISlotStorage - existing_blocks = self.slot.slot_blocks + existing_blocks = self.slot['slot_blocks'] removed_blocks_ids = set(slot_blocks.keys()) - set(existing_blocks.keys()) removed_blocks = {block_id: existing_blocks[block_id] for block_id in diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 7cd3e2b771..51ff499839 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -237,7 +237,7 @@ class ISlotStorage(Interface): class ISlot(Interface): """Slots follow the IBlocks model""" - slot_blocks = JSONField( + blocks = JSONField( title=u"Slot blocks", description=u"The JSON representation of the slot blocks information. Must be a JSON object.", # noqa schema=SLOT_BLOCKS_SCHEMA, @@ -245,7 +245,7 @@ class ISlot(Interface): required=False, ) - slot_blocks_layout = JSONField( + blocks_layout = JSONField( title=u"Slot blocks Layout", description=u"The JSON representation of the slot blocks layout. Must be a JSON array.", # noqa schema=SLOT_LAYOUT_SCHEMA, diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index a810f28f90..cb6f7e05a3 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISlot +from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.serializer.converters import json_compatible from zope.component import adapter @@ -28,11 +28,13 @@ def __init__(self, context, slot, request): self.request = request self.slot = slot - def __call__(self, name): + def __call__(self): + name = self.slot.__name__ + + # a dict with slot_blocks and slot_blocks_layout + data = ISlots(self.context).get_blocks(name) - # TODO: use ISlots - slot_blocks = copy.deepcopy(self.slot_blocks) - slot_blocks_layout = copy.deepcopy(self.slot_blocks_layout) + slot_blocks = copy.deepcopy(data['slot_blocks']) for id, block_value in slot_blocks.items(): block_type = block_value.get("@type", "") @@ -50,9 +52,9 @@ def __call__(self, name): slot_blocks[id] = json_compatible(block_value) return { - "@id": "{0}/{1}/{2}".format(self.context.absolute_url, SERVICE_ID, name), + "@id": "{0}/{1}/{2}".format(self.context.absolute_url(), SERVICE_ID, name), "slot_blocks": slot_blocks, - "slot_blocks_layout": slot_blocks_layout, + "slot_blocks_layout": data['slot_blocks_layout'] } @@ -73,6 +75,6 @@ def __call__(self): serializer = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) - result['items'][name] = serializer(name) + result['items'][name] = serializer() return result diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 9e9bbf2673..36ab464fe2 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -8,10 +8,10 @@ from Products.CMFCore.interfaces import IContentish from zope.annotation.factory import factory from zope.component import adapter +from zope.component import queryAdapter from zope.container.btree import BTreeContainer from zope.container.contained import Contained from zope.interface import implementer -from zope.publisher.interfaces.browser import IBrowserRequest from zope.traversing.interfaces import ITraversable @@ -43,21 +43,23 @@ def __init__(self): @implementer(ISlots) -@adapter(ITraversable, IBrowserRequest) +@adapter(ITraversable) class Slots(object): """ The slots engine provides slots functionality for a content item """ - def __init__(self, context, request): + def __init__(self, context): self.context = context - self.request = request def get_fills_stack(self, name): slot_stack = [] current = self.context while True: - slot = ISlotStorage(current).get(name) + storage = queryAdapter(current, ISlotStorage) + if storage is None: + break + slot = storage.get(name) if slot: slot_stack.append(slot) if current.__parent__: @@ -78,7 +80,7 @@ def get_blocks(self, name): level = 0 for slot in stack: - for uid, block in slot['slot_blocks'].items(): + for uid, block in slot.slot_blocks.items(): block = deepcopy(block) _blockmap[uid] = block @@ -91,7 +93,7 @@ def get_blocks(self, name): if level > 0: block['_v_inherit'] = True - for uid in slot['slot_blocks_layout']['items']: + for uid in slot.slot_blocks_layout['items']: if not (uid in blocks_layout or uid in _replaced): blocks_layout.append(uid) @@ -117,14 +119,16 @@ def _resolve_block(self, block, blocks): def save_data_to_slot(self, slot, data): to_save = {} + for key in data['slot_blocks_layout']['items']: block = data['slot_blocks'][key] if not (block.get('s:sameOf') or block.get('_v_inherit')): to_save[key] = block - for k, v in data.items(): - if k not in ['slot_blocks_layout', 'slot_blocks']: - slot[k] = v + # for k, v in data.items(): + # if k not in ['slot_blocks_layout', 'slot_blocks']: + # slot[k] = v - slot['slot_blocks_layout'] = data['slot_blocks_layout'] - slot['slot_blocks'] = to_save + slot.slot_blocks_layout = data['slot_blocks_layout'] + slot.slot_blocks = to_save + slot._p_changed = True diff --git a/src/plone/restapi/slots.zcml b/src/plone/restapi/slots.zcml new file mode 100644 index 0000000000..b71d3f6baa --- /dev/null +++ b/src/plone/restapi/slots.zcml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py new file mode 100644 index 0000000000..e405364268 --- /dev/null +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# from plone import api +# from plone.app.testing import TEST_USER_ID +from plone.dexterity.utils import createContentInContainer +from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.interfaces import ISlotStorage +from plone.restapi.slots import Slot +from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING +from zope.component import getMultiAdapter + +import unittest + + +class TestSerializeUserToJsonAdapters(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + self.make_content() + + def serialize(self, context, slot): + serializer = getMultiAdapter((context, slot, self.request), ISerializeToJson) + return serializer() + + def make_content(self): + self.documents = createContentInContainer( + self.portal, u"Folder", id=u"documents", title=u"Documents" + ) + self.company = createContentInContainer( + self.documents, u"Folder", id=u"company-a", title=u"Documents" + ) + self.doc = createContentInContainer( + self.company, u"Document", id=u"doc-1", title=u"Doc 1" + ) + + def test_serialize_slots_storage_empty(self): + storage = ISlotStorage(self.portal) + storage['left'] = Slot() + + res = self.serialize(self.portal, storage['left']) + self.assertEqual(res, { + '@id': 'http://nohost/plone/@slots/left', + 'slot_blocks': {}, + 'slot_blocks_layout': {'items': []} + }) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index fb3b5d2318..38f83f8d0c 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import ISlotStorage +from plone.restapi.slots import Slot from plone.restapi.slots import Slots +from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from six.moves import UserDict from zope.component import provideAdapter from zope.interface import implements @@ -53,7 +56,7 @@ def get(self, name): return self.context.slots.get(name) -class Slot(UserDict): +class DummySlot(UserDict): @classmethod def from_data(cls, blocks, layout): @@ -86,11 +89,11 @@ def make_content(self): def test_slot_stack_on_root(self): # simple test with one level stack of slots root = self.make_content() - root.slots['left'] = Slot({ + root.slots['left'] = DummySlot({ 'slot_blocks': {1: {}, 2: {}, 3: {}, }, 'slot_blocks_layout': {'items': [1, 2, 3]} }) - root.slots['right'] = Slot() + root.slots['right'] = DummySlot() engine = Slots(root, None) self.assertEqual(engine.get_fills_stack('bottom'), []) @@ -105,7 +108,7 @@ def test_slot_stack_on_root(self): def test_slot_stack_deep(self): # the slot stack is inherited further down root = self.make_content() - root.slots['left'] = Slot({ + root.slots['left'] = DummySlot({ 'slot_blocks': {1: {}, 2: {}, 3: {}, }, 'slot_blocks_layout': {'items': [1, 2, 3]} }) @@ -121,14 +124,14 @@ def test_slot_stack_deep(self): def test_slot_stack_deep_with_data_in_root(self): # slots stacks up from deepest to shallow root = self.make_content() - root.slots['left'] = Slot({ + root.slots['left'] = DummySlot({ 'slot_blocks': {1: {}, 2: {}, 3: {}, }, 'slot_blocks_layout': {'items': [1, 2, 3]} }) obj = root['documents']['internal']['company-a'] - slot = Slot.from_data({4: {}, 5: {}, 6: {}}, - [4, 5, 6]) + slot = DummySlot.from_data({4: {}, 5: {}, 6: {}}, + [4, 5, 6]) obj.slots['left'] = slot engine = Slots(obj, None) @@ -148,13 +151,13 @@ def test_slot_stack_deep_with_stack_collapse(self): root = self.make_content() obj = root['documents']['internal']['company-a'] - root.slots['left'] = Slot.from_data({1: {}, 2: {}, 3: {}, }, [1, 2, 3]) + root.slots['left'] = DummySlot.from_data({1: {}, 2: {}, 3: {}, }, [1, 2, 3]) - root['documents'].slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}}, - [4, 5, 6]) + root['documents'].slots['left'] = DummySlot.from_data({4: {}, 5: {}, 6: {}}, + [4, 5, 6]) - obj.slots['left'] = Slot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, - [4, 5, 6, 7]) + obj.slots['left'] = DummySlot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, + [4, 5, 6, 7]) engine = Slots(obj, None) @@ -178,8 +181,9 @@ def test_block_data_gets_inherited(self): root = self.make_content() obj = root['documents']['internal'] - root['documents'].slots['left'] = Slot.from_data({1: {'title': 'First'}}, [1]) - obj.slots['left'] = Slot.from_data({2: {}}, [2]) + root['documents'].slots['left'] = DummySlot.from_data( + {1: {'title': 'First'}}, [1]) + obj.slots['left'] = DummySlot.from_data({2: {}}, [2]) engine = Slots(obj, None) left = engine.get_blocks('left') @@ -197,13 +201,13 @@ def test_block_data_gets_override(self): root = self.make_content() - root['documents'].slots['left'] = Slot.from_data({ + root['documents'].slots['left'] = DummySlot.from_data({ 1: {'title': 'First'}, 3: {'title': 'Third'}, }, [1, 3]) obj = root['documents']['internal'] - obj.slots['left'] = Slot.from_data({ + obj.slots['left'] = DummySlot.from_data({ 2: {'s:isVariantOf': 1, 'title': 'Second'}}, [2]) @@ -224,14 +228,14 @@ def test_can_change_order_with_sameOf(self): root = self.make_content() - root['documents'].slots['left'] = Slot.from_data({ + root['documents'].slots['left'] = DummySlot.from_data({ 1: {'title': 'First'}, 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, [5, 1, 3]) obj = root['documents']['internal'] - obj.slots['left'] = Slot.from_data({ + obj.slots['left'] = DummySlot.from_data({ 2: {'s:isVariantOf': 1, 'title': 'Second'}, 4: {'s:sameAs': 3} }, [4, 2]) @@ -254,14 +258,14 @@ def test_can_change_order_from_layout(self): root = self.make_content() - root['documents'].slots['left'] = Slot.from_data({ + root['documents'].slots['left'] = DummySlot.from_data({ 1: {'title': 'First'}, 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, [5, 1, 3]) obj = root['documents']['internal'] - obj.slots['left'] = Slot.from_data({ + obj.slots['left'] = DummySlot.from_data({ 2: {'s:isVariantOf': 1, 'title': 'Second'}, }, [3, 2]) @@ -299,3 +303,45 @@ def test_save_slots(self): 'extra': 'data', 'slot_blocks': {2: {'s:isVariantOf': 1, 'title': 'Second'}}, 'slot_blocks_layout': {'items': [3, 2, 5]}}) + + +class TestSlotsStorage(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + self.make_content() + + def make_content(self): + self.documents = createContentInContainer( + self.portal, u"Folder", id=u"documents", title=u"Documents" + ) + self.company = createContentInContainer( + self.documents, u"Folder", id=u"company-a", title=u"Documents" + ) + self.doc = createContentInContainer( + self.company, u"Document", id=u"doc-1", title=u"Doc 1" + ) + + def test_serialize_slots_storage_portal(self): + storage = ISlotStorage(self.portal) + + self.assertEqual(storage.__name__, 'plone.restapi.slots') + + def test_serialize_slots_storage(self): + storage = ISlotStorage(self.doc) + + self.assertEqual(storage.__name__, 'plone.restapi.slots') + self.assertTrue(storage.__parent__ is self.doc) + self.assertTrue(storage.__parent__ is self.doc) + + def test_store_slots_in_storage(self): + storage = ISlotStorage(self.doc) + storage['left'] = Slot() + + self.assertEqual(storage['left'].__name__, 'left') + self.assertTrue(storage['left'].__parent__ is storage) From 44747ad4bc98348ebdabf356441c81d6158e6813 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 10:38:00 +0200 Subject: [PATCH 17/99] More tests --- src/plone/restapi/slots.py | 5 +- .../restapi/tests/test_serializer_slots.py | 79 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 36ab464fe2..13e9fdf0fd 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -35,12 +35,15 @@ class PersistentSlots(BTreeContainer): class Slot(Contained, Persistent): """A container for data pertaining to a single slot""" - def __init__(self): + def __init__(self, **data): super(Slot, self).__init__() for k, v in deepcopy(DEFAULT_SLOT_DATA).items(): setattr(self, k, v) + for k, v in data.items(): + setattr(self, k, v) + @implementer(ISlots) @adapter(ITraversable) diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index e405364268..5fb3bb7dcb 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -37,7 +37,7 @@ def make_content(self): self.company, u"Document", id=u"doc-1", title=u"Doc 1" ) - def test_serialize_slots_storage_empty(self): + def test_slot_empty(self): storage = ISlotStorage(self.portal) storage['left'] = Slot() @@ -47,3 +47,80 @@ def test_serialize_slots_storage_empty(self): 'slot_blocks': {}, 'slot_blocks_layout': {'items': []} }) + + def test_slot(self): + storage = ISlotStorage(self.portal) + storage['left'] = Slot(**({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + })) + + res = self.serialize(self.portal, storage['left']) + self.assertEqual(res, { + '@id': 'http://nohost/plone/@slots/left', + 'slot_blocks_layout': {'items': [1, 2, 3]}, + 'slot_blocks': {1: {}, 2: {}, 3: {}} + }) + + def test_slot_deep(self): + rootstore = ISlotStorage(self.portal) + rootstore['left'] = Slot(**({ + 'slot_blocks': {1: {}, 2: {}, 3: {}, }, + 'slot_blocks_layout': {'items': [1, 2, 3]} + })) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot() + res = self.serialize(self.doc, storage['left']) + self.assertEqual(res, { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', + 'slot_blocks': {1: {u'_v_inherit': True}, + 2: {u'_v_inherit': True}, + 3: {u'_v_inherit': True}}, + 'slot_blocks_layout': {'items': [1, 2, 3]}}) + + def test_data_override_with_isVariant(self): + rootstore = ISlotStorage(self.portal) + rootstore['left'] = Slot(**({ + 'slot_blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + }, + 'slot_blocks_layout': {'items': [1, 3]} + })) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot( + slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + slot_blocks_layout={'items': [2]}, + ) + res = self.serialize(self.doc, storage['left']) + self.assertEqual(res, { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', + 'slot_blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, + 3: {u'_v_inherit': True, u'title': u'Third'}}, + 'slot_blocks_layout': {'items': [2, 3]}}) + + def test_change_order_from_layout(self): + rootstore = ISlotStorage(self.portal) + rootstore['left'] = Slot(**({ + 'slot_blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'slot_blocks_layout': {'items': [5, 1, 3]} + })) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot( + slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + slot_blocks_layout={'items': [3, 2]}, + ) + res = self.serialize(self.doc, storage['left']) + self.assertEqual(res, { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', + 'slot_blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, + 3: {u'_v_inherit': True, u'title': u'Third'}, + 5: {u'_v_inherit': True, u'title': u'Fifth'}}, + 'slot_blocks_layout': {'items': [3, 2, 5]}}) From 3b1540c301d9a9ccd8d4cec00f6556277c5715a0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 11:42:50 +0200 Subject: [PATCH 18/99] Add discovery of slots, add slot storage serializer --- src/plone/restapi/serializer/slots.py | 20 +++++++- src/plone/restapi/slots.py | 22 ++++++++ .../restapi/tests/test_serializer_slots.py | 51 ++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index cb6f7e05a3..1542f7a160 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -5,6 +5,7 @@ from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.serializer.converters import json_compatible +from plone.restapi.slots import Slot from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers @@ -63,15 +64,30 @@ def __call__(self): class SlotsSerializer(object): """Default slots storage serializer""" + def __init__(self, context, storage, request): + self.context = context + self.request = request + self.storage = storage + def __call__(self): base_url = self.context.absolute_url() result = { '@id': '{}/{}'.format(base_url, SERVICE_ID), "items": {} } - storage = ISlotStorage(self.context) - for name, slot in storage.items(): + engine = ISlots(self.context) + slot_names = engine.discover_slots() + + marker = object() + for name in slot_names: + slot = self.storage.get(name, marker) + + if slot is marker: # if slot is not on this level, we create a fake one + slot = Slot() + slot.__parent__ = self.storage + slot.__name__ = name + serializer = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index 13e9fdf0fd..abdf085561 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -54,6 +54,22 @@ class Slots(object): def __init__(self, context): self.context = context + def discover_slots(self): + current = self.context + names = set() + while True: + storage = queryAdapter(current, ISlotStorage) + if storage is None: + break + for k in storage.keys(): + names.add(k) + if current.__parent__: + current = current.__parent__ + else: + break + + return names + def get_fills_stack(self, name): slot_stack = [] @@ -65,6 +81,8 @@ def get_fills_stack(self, name): slot = storage.get(name) if slot: slot_stack.append(slot) + else: + slot_stack.append(None) if current.__parent__: current = current.__parent__ else: @@ -83,6 +101,10 @@ def get_blocks(self, name): level = 0 for slot in stack: + if slot is None: + level += 1 + continue + for uid, block in slot.slot_blocks.items(): block = deepcopy(block) _blockmap[uid] = block diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index 5fb3bb7dcb..f7f35dd452 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -22,8 +22,9 @@ def setUp(self): self.make_content() - def serialize(self, context, slot): - serializer = getMultiAdapter((context, slot, self.request), ISerializeToJson) + def serialize(self, context, slot_or_storage): + serializer = getMultiAdapter( + (context, slot_or_storage, self.request), ISerializeToJson) return serializer() def make_content(self): @@ -124,3 +125,49 @@ def test_change_order_from_layout(self): 3: {u'_v_inherit': True, u'title': u'Third'}, 5: {u'_v_inherit': True, u'title': u'Fifth'}}, 'slot_blocks_layout': {'items': [3, 2, 5]}}) + + def test_serialize_storage(self): + rootstore = ISlotStorage(self.portal) + rootstore['left'] = Slot(**({ + 'slot_blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'slot_blocks_layout': {'items': [5, 1, 3]} + })) + rootstore['right'] = Slot(**({ + 'slot_blocks': { + 6: {'title': 'First'}, + 7: {'title': 'Third'}, + 8: {'title': 'Fifth'}, + }, + 'slot_blocks_layout': {'items': [8, 6, 7]} + })) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot( + slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + slot_blocks_layout={'items': [3, 2]}, + ) + + res = self.serialize(self.doc, storage) + self.assertEqual(res, { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots', + 'items': { + u'left': { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', + 'slot_blocks': { + 2: {u's:isVariantOf': 1, u'title': u'Second'}, + 3: {u'_v_inherit': True, u'title': u'Third'}, + 5: {u'_v_inherit': True, u'title': u'Fifth'} + }, + 'slot_blocks_layout': {'items': [3, 2, 5]}}, + u'right': { + '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/right', + 'slot_blocks': { + 6: {u'title': u'First', u'_v_inherit': True}, + 7: {u'title': u'Third', u'_v_inherit': True}, + 8: {u'title': u'Fifth', u'_v_inherit': True} + }, + 'slot_blocks_layout': {'items': [8, 6, 7]}}}}) From 6fd2006a53eaa6fd753d5abc3a874e2499b8e6c1 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 12:56:29 +0200 Subject: [PATCH 19/99] Add initial test of slot service --- src/plone/restapi/slots.py | 4 - .../restapi/tests/test_services_slots.py | 108 ++++++++++++++++++ 2 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 src/plone/restapi/tests/test_services_slots.py diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots.py index abdf085561..e28efbe402 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots.py @@ -150,10 +150,6 @@ def save_data_to_slot(self, slot, data): if not (block.get('s:sameOf') or block.get('_v_inherit')): to_save[key] = block - # for k, v in data.items(): - # if k not in ['slot_blocks_layout', 'slot_blocks']: - # slot[k] = v - slot.slot_blocks_layout = data['slot_blocks_layout'] slot.slot_blocks = to_save slot._p_changed = True diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py new file mode 100644 index 0000000000..0fe4404223 --- /dev/null +++ b/src/plone/restapi/tests/test_services_slots.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- + +from plone.app.testing import setRoles +from plone.app.testing import SITE_OWNER_NAME +from plone.app.testing import SITE_OWNER_PASSWORD +from plone.app.testing import TEST_USER_ID +from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING +from plone.restapi.testing import RelativeSession + +import transaction +import unittest + + +# from Products.CMFPlone.tests import dummy +# from zope.component import getUtility +# from zope.interface import directlyProvides +# from zope.interface import noLongerProvides + + +class TestServicesSlots(unittest.TestCase): + + layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING + maxDiff = None + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.portal_url = self.portal.absolute_url() + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + self.api_session = RelativeSession(self.portal_url) + self.api_session.headers.update({"Accept": "application/json"}) + self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) + + self.populateSite() + transaction.commit() + + def tearDown(self): + self.api_session.close() + + def populateSite(self): + """ + Portal + +-doc1 + +-doc2 + +-doc3 + +-folder1 + +-doc11 + +-doc12 + +-doc13 + +-link1 + +-folder2 + +-doc21 + +-doc22 + +-doc23 + +-file21 + +-folder21 + +-doc211 + +-doc212 + """ + setRoles(self.portal, TEST_USER_ID, ["Manager"]) + + if "Members" in self.portal: + self.portal._delObject("Members") + self.folder = None + if "news" in self.portal: + self.portal._delObject("news") + if "events" in self.portal: + self.portal._delObject("events") + if "front-page" in self.portal: + self.portal._delObject("front-page") + if "folder" in self.portal: + self.portal._delObject("folder") + if "users" in self.portal: + self.portal._delObject("users") + + self.portal.invokeFactory("Document", "doc1") + self.portal.invokeFactory("Document", "doc2") + self.portal.invokeFactory("Document", "doc3") + self.portal.invokeFactory("Folder", "folder1") + self.portal.invokeFactory("Link", "link1") + self.portal.link1.remoteUrl = "http://plone.org" + self.portal.link1.reindexObject() + folder1 = getattr(self.portal, "folder1") + folder1.invokeFactory("Document", "doc11") + folder1.invokeFactory("Document", "doc12") + folder1.invokeFactory("Document", "doc13") + self.portal.invokeFactory("Folder", "folder2") + folder2 = getattr(self.portal, "folder2") + folder2.invokeFactory("Document", "doc21") + folder2.invokeFactory("Document", "doc22") + folder2.invokeFactory("Document", "doc23") + folder2.invokeFactory("File", "file21") + folder2.invokeFactory("Folder", "folder21") + folder21 = getattr(folder2, "folder21") + folder21.invokeFactory("Document", "doc211") + folder21.invokeFactory("Document", "doc212") + + setRoles(self.portal, TEST_USER_ID, ["Member"]) + + def test_slots_endpoint(self): + response = self.api_session.get("/@slots") + self.assertEqual(response.status_code, 200) + response = response.json() + self.assertEqual(response, { + "@id": "http://localhost:55001/plone/@slots", + "items": {} + }) From 9b087c5aa51d37ab9197452df1cfbbe1b800f427 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 13:11:27 +0200 Subject: [PATCH 20/99] Fix slot serialization service --- src/plone/restapi/services/slots/get.py | 29 +++++++++++++------ .../restapi/tests/test_services_slots.py | 8 +++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 798faefd62..e9dcf6d219 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- + from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.services import Service +from plone.restapi.slots import Slot from zope.component import getMultiAdapter from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse @@ -33,18 +36,26 @@ def reply(self): def replySlot(self): name = self.params[0] - storage = ISlotStorage(self.context) - try: - slot = storage[name] - except KeyError: + engine = ISlots(self.context) + slot_names = engine.discover_slots() + + if name not in slot_names: self.request.response.setStatus(404) return { "type": "NotFound", "message": 'Slot "{}" could not be found.'.format(self.params[0]), } - finally: - adapter = getMultiAdapter( - (self.context, slot, self.request), ISerializeToJson - ) - return adapter(name) + + marker = object() + storage = ISlotStorage(self.context) + slot = storage.get(name, marker) + if slot is marker: # if slot is not on this level, we create a fake one + slot = Slot() + slot.__parent__ = self.storage + slot.__name__ = name + + adapter = getMultiAdapter( + (self.context, slot, self.request), ISerializeToJson + ) + return adapter(name) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 0fe4404223..8e5ad1109f 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -106,3 +106,11 @@ def test_slots_endpoint(self): "@id": "http://localhost:55001/plone/@slots", "items": {} }) + + def test_slot_endpoint(self): + response = self.api_session.get("/@slots/unregistered") + self.assertEqual(response.status_code, 404) + + def test_slot_endpoint_empty(self): + response = self.api_session.get("/@slots/left") + self.assertEqual(response.status_code, 404) From 6ee8e76a646bf92c6eb78a3f4eaf49f86e900d98 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 15:09:54 +0200 Subject: [PATCH 21/99] Fix slot serializer --- src/plone/restapi/services/slots/get.py | 2 +- .../restapi/tests/test_services_slots.py | 56 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index e9dcf6d219..29a08c6479 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -58,4 +58,4 @@ def replySlot(self): adapter = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) - return adapter(name) + return adapter() diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 8e5ad1109f..91c284ec8a 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -4,6 +4,8 @@ from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID +from plone.restapi.interfaces import ISlotStorage +from plone.restapi.slots import Slot from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import RelativeSession @@ -96,21 +98,61 @@ def populateSite(self): folder21.invokeFactory("Document", "doc211") folder21.invokeFactory("Document", "doc212") + self.doc = self.portal['folder1']['doc11'] + + rootstore = ISlotStorage(self.portal) + rootstore['left'] = Slot(**({ + 'slot_blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'slot_blocks_layout': {'items': [5, 1, 3]} + })) + rootstore['right'] = Slot(**({ + 'slot_blocks': { + 6: {'title': 'First'}, + 7: {'title': 'Third'}, + 8: {'title': 'Fifth'}, + }, + 'slot_blocks_layout': {'items': [8, 6, 7]} + })) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot( + slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + slot_blocks_layout={'items': [3, 2]}, + ) + setRoles(self.portal, TEST_USER_ID, ["Member"]) def test_slots_endpoint(self): response = self.api_session.get("/@slots") self.assertEqual(response.status_code, 200) - response = response.json() - self.assertEqual(response, { - "@id": "http://localhost:55001/plone/@slots", - "items": {} - }) + self.assertEqual(response.json(), { + u'@id': u'http://localhost:55001/plone/@slots', + u'items': {u'left': {u'@id': u'http://localhost:55001/plone/@slots/left', + u'slot_blocks': {u'1': {u'title': u'First'}, + u'3': {u'title': u'Third'}, + u'5': {u'title': u'Fifth'}}, + u'slot_blocks_layout': {u'items': [5, 1, 3]}}, + u'right': {u'@id': u'http://localhost:55001/plone/@slots/right', + u'slot_blocks': {u'6': {u'title': u'First'}, + u'7': {u'title': u'Third'}, + u'8': {u'title': u'Fifth'}}, + u'slot_blocks_layout': {u'items': [8, 6, 7]}}}} + ) def test_slot_endpoint(self): response = self.api_session.get("/@slots/unregistered") self.assertEqual(response.status_code, 404) - def test_slot_endpoint_empty(self): + def test_slot_endpoint_on_root(self): response = self.api_session.get("/@slots/left") - self.assertEqual(response.status_code, 404) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), { + u'@id': u'http://localhost:55001/plone/@slots/left', + u'slot_blocks': {u'1': {u'title': u'First'}, + u'3': {u'title': u'Third'}, + u'5': {u'title': u'Fifth'}}, + u'slot_blocks_layout': {u'items': [5, 1, 3]}}) From 870881d57e90518367bdc90e39c6ba7fbb43a294 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 8 Feb 2021 18:38:24 +0200 Subject: [PATCH 22/99] Add configlet and control panel for content slots --- src/plone/restapi/configure.zcml | 2 +- src/plone/restapi/controlpanels/registry.py | 7 +++-- .../restapi/profiles/default/controlpanel.xml | 20 +++++++++++++ .../restapi/profiles/default/registry.xml | 4 +++ src/plone/restapi/services/slots/get.py | 9 ++++-- .../restapi/{slots.py => slots/__init__.py} | 10 +++++-- .../{slots.zcml => slots/configure.zcml} | 29 ++++++++++++++----- src/plone/restapi/slots/controlpanel.py | 25 ++++++++++++++++ src/plone/restapi/{ => slots}/events.py | 0 src/plone/restapi/slots/interfaces.py | 13 +++++++++ .../restapi/tests/test_services_slots.py | 6 ---- .../upgrades/profiles/0007/controlpanel.xml | 20 +++++++++++++ .../upgrades/profiles/0007/registry.xml | 4 +++ 13 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 src/plone/restapi/profiles/default/controlpanel.xml create mode 100644 src/plone/restapi/profiles/default/registry.xml rename src/plone/restapi/{slots.py => slots/__init__.py} (95%) rename src/plone/restapi/{slots.zcml => slots/configure.zcml} (54%) create mode 100644 src/plone/restapi/slots/controlpanel.py rename src/plone/restapi/{ => slots}/events.py (100%) create mode 100644 src/plone/restapi/slots/interfaces.py create mode 100644 src/plone/restapi/upgrades/profiles/0007/controlpanel.xml create mode 100644 src/plone/restapi/upgrades/profiles/0007/registry.xml diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index 231fb397ce..f18525bf85 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -23,7 +23,6 @@ - + diff --git a/src/plone/restapi/controlpanels/registry.py b/src/plone/restapi/controlpanels/registry.py index f9bbce70a3..60630b45d7 100644 --- a/src/plone/restapi/controlpanels/registry.py +++ b/src/plone/restapi/controlpanels/registry.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from zope.component import adapter -from zope.interface import Interface +from plone.restapi.controlpanels import RegistryConfigletPanel from Products.CMFPlone.interfaces.controlpanel import IDateAndTimeSchema from Products.CMFPlone.interfaces.controlpanel import IEditingSchema from Products.CMFPlone.interfaces.controlpanel import IImagingSchema @@ -11,7 +10,9 @@ from Products.CMFPlone.interfaces.controlpanel import ISecuritySchema from Products.CMFPlone.interfaces.controlpanel import ISiteSchema from Products.CMFPlone.interfaces.controlpanel import ISocialMediaSchema -from plone.restapi.controlpanels import RegistryConfigletPanel +from zope.component import adapter +from zope.interface import Interface + try: from plone.i18n.interfaces import ILanguageSchema diff --git a/src/plone/restapi/profiles/default/controlpanel.xml b/src/plone/restapi/profiles/default/controlpanel.xml new file mode 100644 index 0000000000..37d29c3ff5 --- /dev/null +++ b/src/plone/restapi/profiles/default/controlpanel.xml @@ -0,0 +1,20 @@ + + + + + Manage portal + + + diff --git a/src/plone/restapi/profiles/default/registry.xml b/src/plone/restapi/profiles/default/registry.xml new file mode 100644 index 0000000000..b52e49c1be --- /dev/null +++ b/src/plone/restapi/profiles/default/registry.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 29a08c6479..e64e60e2bf 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -55,7 +55,10 @@ def replySlot(self): slot.__parent__ = self.storage slot.__name__ = name - adapter = getMultiAdapter( + result = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson - ) - return adapter() + )() + + result['can_edit'] = can_edit_slots(self.context) + + return result diff --git a/src/plone/restapi/slots.py b/src/plone/restapi/slots/__init__.py similarity index 95% rename from src/plone/restapi/slots.py rename to src/plone/restapi/slots/__init__.py index e28efbe402..1d8b892dce 100644 --- a/src/plone/restapi/slots.py +++ b/src/plone/restapi/slots/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from .interfaces import ISlot -from .interfaces import ISlots -from .interfaces import ISlotStorage from copy import deepcopy from persistent import Persistent +from plone.restapi.interfaces import ISlot +from plone.restapi.interfaces import ISlots +from plone.restapi.interfaces import ISlotStorage from Products.CMFCore.interfaces import IContentish from zope.annotation.factory import factory from zope.component import adapter @@ -153,3 +153,7 @@ def save_data_to_slot(self, slot, data): slot.slot_blocks_layout = data['slot_blocks_layout'] slot.slot_blocks = to_save slot._p_changed = True + + +def can_edit_slots(context): + pass diff --git a/src/plone/restapi/slots.zcml b/src/plone/restapi/slots/configure.zcml similarity index 54% rename from src/plone/restapi/slots.zcml rename to src/plone/restapi/slots/configure.zcml index b71d3f6baa..9527432c92 100644 --- a/src/plone/restapi/slots.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -1,5 +1,6 @@ + + + + diff --git a/src/plone/restapi/slots/controlpanel.py b/src/plone/restapi/slots/controlpanel.py new file mode 100644 index 0000000000..15616c2597 --- /dev/null +++ b/src/plone/restapi/slots/controlpanel.py @@ -0,0 +1,25 @@ +from .interfaces import ISlotSettings +from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper +from plone.app.registry.browser.controlpanel import RegistryEditForm +from plone.restapi.controlpanels import RegistryConfigletPanel +from plone.z3cform import layout +from z3c.form import form +from zope.component import adapter +from zope.interface import Interface + + +class SlotsControlPanelForm(RegistryEditForm): + form.extends(RegistryEditForm) + schema = ISlotSettings + + +SlotsControlPanelView = layout.wrap_form(SlotsControlPanelForm, ControlPanelFormWrapper) +SlotsControlPanelView.label = u"Slots" + + +@adapter(Interface, Interface) +class SlotsControlpanel(RegistryConfigletPanel): + schema = ISlotSettings + + configlet_id = "SlotSettings" + configlet_category_id = "plone-content" diff --git a/src/plone/restapi/events.py b/src/plone/restapi/slots/events.py similarity index 100% rename from src/plone/restapi/events.py rename to src/plone/restapi/slots/events.py diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py new file mode 100644 index 0000000000..ab2c57266f --- /dev/null +++ b/src/plone/restapi/slots/interfaces.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from zope.interface import Interface +from zope.schema import List +from zope.schema import TextLine + + +class ISlotSettings(Interface): + content_slots = List( + title=u"Content slots", + description=u'Editable slots using "Modify portal content" permission', + value_type=TextLine(title=u"Slot name") + ) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 91c284ec8a..c87a590b99 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -13,12 +13,6 @@ import unittest -# from Products.CMFPlone.tests import dummy -# from zope.component import getUtility -# from zope.interface import directlyProvides -# from zope.interface import noLongerProvides - - class TestServicesSlots(unittest.TestCase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING diff --git a/src/plone/restapi/upgrades/profiles/0007/controlpanel.xml b/src/plone/restapi/upgrades/profiles/0007/controlpanel.xml new file mode 100644 index 0000000000..37d29c3ff5 --- /dev/null +++ b/src/plone/restapi/upgrades/profiles/0007/controlpanel.xml @@ -0,0 +1,20 @@ + + + + + Manage portal + + + diff --git a/src/plone/restapi/upgrades/profiles/0007/registry.xml b/src/plone/restapi/upgrades/profiles/0007/registry.xml new file mode 100644 index 0000000000..b52e49c1be --- /dev/null +++ b/src/plone/restapi/upgrades/profiles/0007/registry.xml @@ -0,0 +1,4 @@ + + + + From 62676e4e54e8119b5782b503793651d7059d514d Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 9 Feb 2021 07:46:19 +0200 Subject: [PATCH 23/99] Shuffle events code --- src/plone/restapi/events.py | 18 +++++++++++++++++ src/plone/restapi/slots/configure.zcml | 4 ++-- .../restapi/slots/{events.py => handlers.py} | 20 ++----------------- 3 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 src/plone/restapi/events.py rename src/plone/restapi/slots/{events.py => handlers.py} (77%) diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py new file mode 100644 index 0000000000..6736db44d0 --- /dev/null +++ b/src/plone/restapi/events.py @@ -0,0 +1,18 @@ +from plone.restapi.interfaces import IBlockRemovedEvent +from plone.restapi.interfaces import IBlocksRemovedEvent +from zope.interface import implements +from zope.interface.interfaces import ObjectEvent + + +class BlockRemovedEvent(ObjectEvent): + implements(IBlockRemovedEvent) + + def __init__(self, object): + self.object = object + + +class BlocksRemovedEvent(ObjectEvent): + implements(IBlocksRemovedEvent) + + def __init__(self, object): + self.object = object diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index 9527432c92..81a23d0c93 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -32,11 +32,11 @@ Date: Tue, 9 Feb 2021 11:44:26 +0200 Subject: [PATCH 24/99] Add get_editable_slots implementation --- src/plone/restapi/events.py | 2 ++ src/plone/restapi/permissions.py | 2 ++ src/plone/restapi/permissions.zcml | 6 ++--- src/plone/restapi/services/slots/get.py | 14 +++++++----- src/plone/restapi/slots/__init__.py | 22 +++++++++++++++++-- src/plone/restapi/slots/controlpanel.py | 2 ++ .../restapi/tests/test_services_slots.py | 1 + 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py index 6736db44d0..03d8c5a882 100644 --- a/src/plone/restapi/events.py +++ b/src/plone/restapi/events.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from plone.restapi.interfaces import IBlockRemovedEvent from plone.restapi.interfaces import IBlocksRemovedEvent from zope.interface import implements diff --git a/src/plone/restapi/permissions.py b/src/plone/restapi/permissions.py index db97904821..a89ea6f3a7 100644 --- a/src/plone/restapi/permissions.py +++ b/src/plone/restapi/permissions.py @@ -4,3 +4,5 @@ # permissions. Granted to Anonymous (i.e. everyone) by default via rolemap.xml UseRESTAPI = "plone.restapi: Use REST API" + +ModifySlotsPermission = "plone.restapi: Modify slots information" diff --git a/src/plone/restapi/permissions.zcml b/src/plone/restapi/permissions.zcml index 76989540e7..1f16fdb21c 100644 --- a/src/plone/restapi/permissions.zcml +++ b/src/plone/restapi/permissions.zcml @@ -18,13 +18,13 @@ title="plone.restapi: Access Plone user information" /> diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index e64e60e2bf..cc313faf85 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -23,6 +23,10 @@ def publishTraverse(self, request, name): return self def reply(self): + self.engine = ISlots(self.context) + self.slot_names = self.engine.discover_slots() + self.editable_slots = self.engine.get_editable_slots() + if self.params and len(self.params) > 0: return self.replySlot() @@ -37,10 +41,7 @@ def reply(self): def replySlot(self): name = self.params[0] - engine = ISlots(self.context) - slot_names = engine.discover_slots() - - if name not in slot_names: + if name not in self.slot_names: self.request.response.setStatus(404) return { "type": "NotFound", @@ -51,7 +52,7 @@ def replySlot(self): storage = ISlotStorage(self.context) slot = storage.get(name, marker) if slot is marker: # if slot is not on this level, we create a fake one - slot = Slot() + slot = Slot() # TODO: replace with a DummyProxySlot slot.__parent__ = self.storage slot.__name__ = name @@ -59,6 +60,7 @@ def replySlot(self): (self.context, slot, self.request), ISerializeToJson )() - result['can_edit'] = can_edit_slots(self.context) + result['can_edit'] = name in self.editable_slots + # TODO: add transaction doom, to deal with annotations created by ISlotStorage ? return result diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 1d8b892dce..93f276736d 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -1,13 +1,18 @@ # -*- coding: utf-8 -*- +from AccessControl.SecurityManagement import getSecurityManager from copy import deepcopy from persistent import Persistent +from plone.registry.interfaces import IRegistry from plone.restapi.interfaces import ISlot from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage +from plone.restapi.permissions import ModifySlotsPermission +from plone.restapi.slots.interfaces import ISlotSettings from Products.CMFCore.interfaces import IContentish from zope.annotation.factory import factory from zope.component import adapter +from zope.component import getUtility from zope.component import queryAdapter from zope.container.btree import BTreeContainer from zope.container.contained import Contained @@ -154,6 +159,19 @@ def save_data_to_slot(self, slot, data): slot.slot_blocks = to_save slot._p_changed = True + def get_editable_slots(self): + sm = getSecurityManager() -def can_edit_slots(context): - pass + slot_names = self.discover_slots() + + if sm.checkPermission(ModifySlotsPermission, self.context): + return slot_names + + if not sm.checkPermission("Modify portal content", self.context): + return [] + + registry = getUtility(IRegistry) + records = registry.forInterface(ISlotSettings) + + content_slots = filter(None, [b.strip() for b in records.content_slots]) + return content_slots diff --git a/src/plone/restapi/slots/controlpanel.py b/src/plone/restapi/slots/controlpanel.py index 15616c2597..d3b8112d91 100644 --- a/src/plone/restapi/slots/controlpanel.py +++ b/src/plone/restapi/slots/controlpanel.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from .interfaces import ISlotSettings from plone.app.registry.browser.controlpanel import ControlPanelFormWrapper from plone.app.registry.browser.controlpanel import RegistryEditForm diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index c87a590b99..badfd5f402 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -146,6 +146,7 @@ def test_slot_endpoint_on_root(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), { u'@id': u'http://localhost:55001/plone/@slots/left', + u'can_edit': True, u'slot_blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, From a8a0d9809af7013916142f9332827d96bf877e4a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 9 Feb 2021 14:57:16 +0200 Subject: [PATCH 25/99] Pass editable status in @slots endpoints --- src/plone/restapi/services/slots/get.py | 12 ++++++++++-- src/plone/restapi/tests/test_services_slots.py | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index cc313faf85..c359a7e660 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -3,6 +3,7 @@ from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage +from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service from plone.restapi.slots import Slot from zope.component import getMultiAdapter @@ -35,8 +36,15 @@ def reply(self): adapter = getMultiAdapter( (self.context, storage, self.request), ISerializeToJson ) + result = adapter() - return adapter() + # update "edit:True" editable status in slots + result['edit_slots'] = json_compatible(self.editable_slots) + + # for k, v in result['items'].items(): + # result['items'][k]['edit'] = k in self.editable_slots + + return result def replySlot(self): name = self.params[0] @@ -60,7 +68,7 @@ def replySlot(self): (self.context, slot, self.request), ISerializeToJson )() - result['can_edit'] = name in self.editable_slots + result['edit'] = name in self.editable_slots # TODO: add transaction doom, to deal with annotations created by ISlotStorage ? return result diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index badfd5f402..5d77d926a6 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -125,6 +125,7 @@ def test_slots_endpoint(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), { u'@id': u'http://localhost:55001/plone/@slots', + u'edit_slots': [u'right', u'left'], u'items': {u'left': {u'@id': u'http://localhost:55001/plone/@slots/left', u'slot_blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, @@ -146,7 +147,7 @@ def test_slot_endpoint_on_root(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.json(), { u'@id': u'http://localhost:55001/plone/@slots/left', - u'can_edit': True, + u'edit': True, u'slot_blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, From 53953af3c3950043e5545261b75be48593728f97 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 9 Feb 2021 15:44:59 +0200 Subject: [PATCH 26/99] Fix tests --- src/plone/restapi/services/slots/get.py | 2 +- src/plone/restapi/tests/test_slots.py | 86 ++++++++++++++----------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index c359a7e660..afca3efebc 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -38,9 +38,9 @@ def reply(self): ) result = adapter() - # update "edit:True" editable status in slots result['edit_slots'] = json_compatible(self.editable_slots) + # update "edit:True" editable status in slots # for k, v in result['items'].items(): # result['items'][k]['edit'] = k in self.editable_slots diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 38f83f8d0c..e140f835e1 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -56,7 +56,10 @@ def get(self, name): return self.context.slots.get(name) -class DummySlot(UserDict): +class DummySlot(object): + def __init__(self, data={}): + self.slot_blocks_layout = data and data['slot_blocks_layout'] or {"items": []} + self.slot_blocks = data and data['slot_blocks'] or {} @classmethod def from_data(cls, blocks, layout): @@ -94,16 +97,19 @@ def test_slot_stack_on_root(self): 'slot_blocks_layout': {'items': [1, 2, 3]} }) root.slots['right'] = DummySlot() - engine = Slots(root, None) - - self.assertEqual(engine.get_fills_stack('bottom'), []) - self.assertEqual(engine.get_fills_stack('right'), []) - self.assertEqual(engine.get_fills_stack('left'), [ - { - 'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': {1: {}, 2: {}, 3: {}} - } - ]) + engine = Slots(root) + + self.assertEqual(engine.get_fills_stack('bottom'), [None]) + + right_stack = engine.get_fills_stack('right') + self.assertEqual(len(right_stack), 1) + self.assertEqual(right_stack[0].slot_blocks_layout, {"items": []}) + self.assertEqual(right_stack[0].slot_blocks, {}) + + left_stack = engine.get_fills_stack('left') + self.assertEqual(len(left_stack), 1) + self.assertEqual(left_stack[0].slot_blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left_stack[0].slot_blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep(self): # the slot stack is inherited further down @@ -112,14 +118,17 @@ def test_slot_stack_deep(self): 'slot_blocks': {1: {}, 2: {}, 3: {}, }, 'slot_blocks_layout': {'items': [1, 2, 3]} }) - engine = Slots(root['documents']['internal']['company-a'], None) + engine = Slots(root['documents']['internal']['company-a']) - self.assertEqual(engine.get_fills_stack('bottom'), []) - self.assertEqual(engine.get_fills_stack('right'), []) - self.assertEqual(engine.get_fills_stack('left'), [ - {'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': {1: {}, 2: {}, 3: {}}} - ]) + self.assertEqual(engine.get_fills_stack('bottom'), [None, None, None, None]) + self.assertEqual(engine.get_fills_stack('right'), [None, None, None, None]) + + left_stack = engine.get_fills_stack('left') + self.assertEqual(len(left_stack), 4) + + left = left_stack[3] + self.assertEqual(left.slot_blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left.slot_blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_data_in_root(self): # slots stacks up from deepest to shallow @@ -134,16 +143,18 @@ def test_slot_stack_deep_with_data_in_root(self): [4, 5, 6]) obj.slots['left'] = slot - engine = Slots(obj, None) + engine = Slots(obj) + stack = engine.get_fills_stack('left') + + self.assertEqual(stack[1:3], [None, None]) + + first = stack[0] + self.assertEqual(first.slot_blocks_layout, {'items': [4, 5, 6]}) + self.assertEqual(first.slot_blocks, {4: {}, 5: {}, 6: {}}) - self.assertEqual(engine.get_fills_stack('left'), [ - { - 'slot_blocks_layout': {'items': [4, 5, 6]}, - 'slot_blocks': {4: {}, 5: {}, 6: {}}}, - { - 'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': {1: {}, 2: {}, 3: {}}} - ]) + last = stack[3] + self.assertEqual(last.slot_blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(last.slot_blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_stack_collapse(self): # get_blocks collapses the stack and marks inherited slots with _v_inherit @@ -159,7 +170,7 @@ def test_slot_stack_deep_with_stack_collapse(self): obj.slots['left'] = DummySlot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, [4, 5, 6, 7]) - engine = Slots(obj, None) + engine = Slots(obj) left = engine.get_blocks('left') self.assertEqual(left, { @@ -185,7 +196,7 @@ def test_block_data_gets_inherited(self): {1: {'title': 'First'}}, [1]) obj.slots['left'] = DummySlot.from_data({2: {}}, [2]) - engine = Slots(obj, None) + engine = Slots(obj) left = engine.get_blocks('left') self.assertEqual(left, { @@ -211,7 +222,7 @@ def test_block_data_gets_override(self): 2: {'s:isVariantOf': 1, 'title': 'Second'}}, [2]) - engine = Slots(obj, None) + engine = Slots(obj) left = engine.get_blocks('left') self.assertEqual(left, { @@ -240,7 +251,7 @@ def test_can_change_order_with_sameOf(self): 4: {'s:sameAs': 3} }, [4, 2]) - engine = Slots(obj, None) + engine = Slots(obj) left = engine.get_blocks('left') self.assertEqual(left, { @@ -269,7 +280,7 @@ def test_can_change_order_from_layout(self): 2: {'s:isVariantOf': 1, 'title': 'Second'}, }, [3, 2]) - engine = Slots(obj, None) + engine = Slots(obj) left = engine.get_blocks('left') self.assertEqual(left, { @@ -289,20 +300,17 @@ def test_save_slots(self): 3: {'title': 'Third', '_v_inherit': True}, 5: {'title': 'Fifth', '_v_inherit': True}, }, - 'extra': 'data', } root = self.make_content() obj = root['documents']['internal'] - engine = Slots(obj, None) + engine = Slots(obj) - slot = {} + slot = DummySlot() engine.save_data_to_slot(slot, data) - self.assertEqual(slot, { - 'extra': 'data', - 'slot_blocks': {2: {'s:isVariantOf': 1, 'title': 'Second'}}, - 'slot_blocks_layout': {'items': [3, 2, 5]}}) + self.assertEqual(slot.slot_blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) + self.assertEqual(slot.slot_blocks_layout, {'items': [3, 2, 5]}) class TestSlotsStorage(unittest.TestCase): From 58ccfbcc8b7e1d2dbe33617cc7366671b8d8ce55 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 9 Feb 2021 15:45:19 +0200 Subject: [PATCH 27/99] Fix tests --- src/plone/restapi/tests/test_slots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index e140f835e1..a53f33f8df 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -5,7 +5,6 @@ from plone.restapi.slots import Slot from plone.restapi.slots import Slots from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING -from six.moves import UserDict from zope.component import provideAdapter from zope.interface import implements from zope.interface import Interface From ffa86ba9827ca32434dbaf4fca3e8ee6d8060374 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 00:10:16 +0200 Subject: [PATCH 28/99] Add security tests --- src/plone/restapi/slots/__init__.py | 6 +- src/plone/restapi/tests/test_slots.py | 88 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 93f276736d..db1096f302 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -165,7 +165,7 @@ def get_editable_slots(self): slot_names = self.discover_slots() if sm.checkPermission(ModifySlotsPermission, self.context): - return slot_names + return list(slot_names) if not sm.checkPermission("Modify portal content", self.context): return [] @@ -173,5 +173,7 @@ def get_editable_slots(self): registry = getUtility(IRegistry) records = registry.forInterface(ISlotSettings) - content_slots = filter(None, [b.strip() for b in records.content_slots]) + content_slots = [s for s in + [line.strip() for line in (records.content_slots or [])] + if s in slot_names] return content_slots diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index a53f33f8df..4faed1d42c 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -1,10 +1,15 @@ # -*- coding: utf-8 -*- +from plone.api import portal from plone.dexterity.utils import createContentInContainer +from plone.registry.interfaces import IRegistry +from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot from plone.restapi.slots import Slots +from plone.restapi.slots.interfaces import ISlotSettings from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING +from Products.CMFPlone.tests.PloneTestCase import PloneTestCase from zope.component import provideAdapter from zope.interface import implements from zope.interface import Interface @@ -352,3 +357,86 @@ def test_store_slots_in_storage(self): self.assertEqual(storage['left'].__name__, 'left') self.assertTrue(storage['left'].__parent__ is storage) + + +class TestSlotsEngineIntegration(PloneTestCase): + + layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + self.portal.acl_users.userFolderAddUser( + 'simple_member', 'slots_pw', ["Member"], [] + ) + + self.portal.acl_users.userFolderAddUser( + 'editor_member', 'slots_pw', ["Editor"], [] + ) + + self.make_content() + + def make_content(self): + self.documents = createContentInContainer( + self.portal, u"Folder", id=u"documents", title=u"Documents" + ) + self.company = createContentInContainer( + self.documents, u"Folder", id=u"company-a", title=u"Documents" + ) + self.doc = createContentInContainer( + self.company, u"Document", id=u"doc-1", title=u"Doc 1" + ) + + def test_editable_slots_as_manager(self): + engine = ISlots(self.doc) + + empty = engine.get_editable_slots() + self.assertEqual(empty, []) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot() + + left = engine.get_editable_slots() + self.assertEqual(left, ['left']) + + def test_editable_slots_as_member(self): + self.login('simple_member') + + engine = ISlots(self.doc) + + empty = engine.get_editable_slots() + self.assertEqual(empty, []) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot() + + left = engine.get_editable_slots() + self.assertEqual(left, []) + + registry = portal.get_tool('portal_registry') + proxy = registry.forInterface(ISlotSettings) + proxy.content_slots = [u'left'] + + self.assertEqual(engine.get_editable_slots(), []) + + def test_editable_slots_as_editor(self): + self.login('editor_member') + + engine = ISlots(self.doc) + + empty = engine.get_editable_slots() + self.assertEqual(empty, []) + + storage = ISlotStorage(self.doc) + storage['left'] = Slot() + + left = engine.get_editable_slots() + self.assertEqual(left, []) + + registry = portal.get_tool('portal_registry') + proxy = registry.forInterface(ISlotSettings) + proxy.content_slots = [u'left'] + + self.assertEqual(engine.get_editable_slots(), [u'left']) From 84b5779341e411f22ac26b71112fb799ee972c74 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 00:11:09 +0200 Subject: [PATCH 29/99] Add security tests --- src/plone/restapi/tests/test_slots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 4faed1d42c..c4bbdd0b89 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -2,7 +2,6 @@ from plone.api import portal from plone.dexterity.utils import createContentInContainer -from plone.registry.interfaces import IRegistry from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot From f56b43adf2639ab71a2c742ce2f8bb7be78fb9e0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 09:48:59 +0200 Subject: [PATCH 30/99] WIP on deserializer --- src/plone/restapi/deserializer/slots.py | 15 +++++------ .../restapi/tests/test_services_slots.py | 18 +++---------- src/plone/restapi/tests/test_slots.py | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index f2dc8c31d9..717a489aff 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -37,9 +37,9 @@ def __call__(self, data=None): slot_blocks = copy.deepcopy(data['slot_blocks']) - existing_blocks = self.slot['slot_blocks'] + existing_blocks = self.slot.slot_blocks - removed_blocks_ids = set(slot_blocks.keys()) - set(existing_blocks.keys()) + removed_blocks_ids = set(existing_blocks.keys()) - set(slot_blocks.keys()) removed_blocks = {block_id: existing_blocks[block_id] for block_id in removed_blocks_ids} @@ -62,11 +62,8 @@ def __call__(self, data=None): slot_blocks[id] = block_value - data['slot_blocks'] = slot_blocks - - for k, v in data.items(): - self.slot[k] = v - + self.slot.slot_blocks = slot_blocks + self.slot.slot_blocks_layout = data['slot_blocks_layout'] self.slot._p_changed = True @@ -77,7 +74,7 @@ class SlotDeserializerRoot(SlotDeserializer): @adapter(IContentish, ISlotStorage, IBrowserRequest) -@implementer(IBlockFieldDeserializationTransformer) +@implementer(IDeserializeFromJson) class SlotsDeserializer(object): """ Default deserializer of slots """ @@ -111,7 +108,7 @@ def __call__(self, data=None): deserializer = getMultiAdapter( (self.context, slot, self.request), IDeserializeFromJson ) - deserializer() + deserializer(slotdata) @adapter(IPloneSiteRoot, ISlotStorage, IBrowserRequest) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 5d77d926a6..51e19dcc39 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -56,20 +56,6 @@ def populateSite(self): """ setRoles(self.portal, TEST_USER_ID, ["Manager"]) - if "Members" in self.portal: - self.portal._delObject("Members") - self.folder = None - if "news" in self.portal: - self.portal._delObject("news") - if "events" in self.portal: - self.portal._delObject("events") - if "front-page" in self.portal: - self.portal._delObject("front-page") - if "folder" in self.portal: - self.portal._delObject("folder") - if "users" in self.portal: - self.portal._delObject("users") - self.portal.invokeFactory("Document", "doc1") self.portal.invokeFactory("Document", "doc2") self.portal.invokeFactory("Document", "doc3") @@ -152,3 +138,7 @@ def test_slot_endpoint_on_root(self): u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, u'slot_blocks_layout': {u'items': [5, 1, 3]}}) + + # def test_deserializer_slot_not_found(self): + # response = self.api_session.patch('/@slots/left', json={}) + # self.assertEqual(response.status_code, 404) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index c4bbdd0b89..178cc408b8 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -2,6 +2,7 @@ from plone.api import portal from plone.dexterity.utils import createContentInContainer +from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.interfaces import ISlots from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot @@ -9,6 +10,7 @@ from plone.restapi.slots.interfaces import ISlotSettings from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase +from zope.component import getMultiAdapter from zope.component import provideAdapter from zope.interface import implements from zope.interface import Interface @@ -439,3 +441,28 @@ def test_editable_slots_as_editor(self): proxy.content_slots = [u'left'] self.assertEqual(engine.get_editable_slots(), [u'left']) + + def test_deserialize_empty(self): + storage = ISlotStorage(self.doc) + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + deserializer() + + self.assertEqual(list(storage.keys()), []) + + def test_deserialize_put_one(self): + storage = ISlotStorage(self.doc) + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + deserializer({"left": { + 'slot_blocks_layout': {'items': [3, 2, 5]}, + 'slot_blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + } + }}) + + self.assertEqual(list(storage.keys()), ['left']) + import pdb + pdb.set_trace() From 6e27022e2911415ed5d25acd3e43016473c439b6 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 09:54:23 +0200 Subject: [PATCH 31/99] Rename slot_blocks -> blocks, slot_blocks_layout -> blocks_layout, as we need IBlocks compabitility with transformers --- src/plone/restapi/deserializer/slots.py | 16 ++-- src/plone/restapi/indexers.py | 2 +- src/plone/restapi/serializer/slots.py | 12 +-- src/plone/restapi/slots/__init__.py | 20 ++--- src/plone/restapi/slots/handlers.py | 12 +-- .../restapi/tests/test_serializer_slots.py | 64 ++++++++-------- .../restapi/tests/test_services_slots.py | 24 +++--- src/plone/restapi/tests/test_slots.py | 74 +++++++++---------- 8 files changed, 111 insertions(+), 113 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 717a489aff..1fa05b67dc 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -35,17 +35,17 @@ def __call__(self, data=None): if data is None: data = json_body(self.request) - slot_blocks = copy.deepcopy(data['slot_blocks']) + blocks = copy.deepcopy(data['blocks']) - existing_blocks = self.slot.slot_blocks + existing_blocks = self.slot.blocks - removed_blocks_ids = set(existing_blocks.keys()) - set(slot_blocks.keys()) + removed_blocks_ids = set(existing_blocks.keys()) - set(blocks.keys()) removed_blocks = {block_id: existing_blocks[block_id] for block_id in removed_blocks_ids} notify(BlocksRemovedEvent(dict(context=self.context, blocks=removed_blocks))) - for id, block_value in slot_blocks.items(): + for id, block_value in blocks.items(): block_type = block_value.get("@type", "") handlers = [] @@ -60,10 +60,10 @@ def __call__(self, data=None): if not getattr(handler, "disabled", False): block_value = handler(block_value) - slot_blocks[id] = block_value + blocks[id] = block_value - self.slot.slot_blocks = slot_blocks - self.slot.slot_blocks_layout = data['slot_blocks_layout'] + self.slot.blocks = blocks + self.slot.blocks_layout = data['blocks_layout'] self.slot._p_changed = True @@ -93,7 +93,7 @@ def __call__(self, data=None): if slotdata is None: notify(BlocksRemovedEvent(dict( context=self.context, - blocks=slot.slot_blocks + blocks=slot.blocks ))) del self.storage[name] diff --git a/src/plone/restapi/indexers.py b/src/plone/restapi/indexers.py index 6c3f9e5570..105cc0737e 100644 --- a/src/plone/restapi/indexers.py +++ b/src/plone/restapi/indexers.py @@ -87,6 +87,6 @@ def slot_block_ids(obj): blocks = [] storage = ISlots(obj) for name, slot in storage.items(): - blocks.extend(slot.slot_blocks_layout['items']) + blocks.extend(slot.blocks_layout['items']) return blocks diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 1542f7a160..558425c7fc 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -32,12 +32,12 @@ def __init__(self, context, slot, request): def __call__(self): name = self.slot.__name__ - # a dict with slot_blocks and slot_blocks_layout + # a dict with blocks and blocks_layout data = ISlots(self.context).get_blocks(name) - slot_blocks = copy.deepcopy(data['slot_blocks']) + blocks = copy.deepcopy(data['blocks']) - for id, block_value in slot_blocks.items(): + for id, block_value in blocks.items(): block_type = block_value.get("@type", "") handlers = [] for h in subscribers( @@ -50,12 +50,12 @@ def __call__(self): if not getattr(handler, "disabled", False): block_value = handler(block_value) - slot_blocks[id] = json_compatible(block_value) + blocks[id] = json_compatible(block_value) return { "@id": "{0}/{1}/{2}".format(self.context.absolute_url(), SERVICE_ID, name), - "slot_blocks": slot_blocks, - "slot_blocks_layout": data['slot_blocks_layout'] + "blocks": blocks, + "blocks_layout": data['blocks_layout'] } diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index db1096f302..2e74385210 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -22,8 +22,8 @@ SLOTS_KEY = "plone.restapi.slots" DEFAULT_SLOT_DATA = { - "slot_blocks_layout": {"items": []}, - "slot_blocks": {} + "blocks_layout": {"items": []}, + "blocks": {} } @@ -110,7 +110,7 @@ def get_blocks(self, name): level += 1 continue - for uid, block in slot.slot_blocks.items(): + for uid, block in slot.blocks.items(): block = deepcopy(block) _blockmap[uid] = block @@ -123,7 +123,7 @@ def get_blocks(self, name): if level > 0: block['_v_inherit'] = True - for uid in slot.slot_blocks_layout['items']: + for uid in slot.blocks_layout['items']: if not (uid in blocks_layout or uid in _replaced): blocks_layout.append(uid) @@ -135,8 +135,8 @@ def get_blocks(self, name): v.update(self._resolve_block(v, _blockmap)) return { - 'slot_blocks': blocks, - 'slot_blocks_layout': {'items': blocks_layout} + 'blocks': blocks, + 'blocks_layout': {'items': blocks_layout} } def _resolve_block(self, block, blocks): @@ -150,13 +150,13 @@ def _resolve_block(self, block, blocks): def save_data_to_slot(self, slot, data): to_save = {} - for key in data['slot_blocks_layout']['items']: - block = data['slot_blocks'][key] + for key in data['blocks_layout']['items']: + block = data['blocks'][key] if not (block.get('s:sameOf') or block.get('_v_inherit')): to_save[key] = block - slot.slot_blocks_layout = data['slot_blocks_layout'] - slot.slot_blocks = to_save + slot.blocks_layout = data['blocks_layout'] + slot.blocks = to_save slot._p_changed = True def get_editable_slots(self): diff --git a/src/plone/restapi/slots/handlers.py b/src/plone/restapi/slots/handlers.py index 91b45daf97..07393df34b 100644 --- a/src/plone/restapi/slots/handlers.py +++ b/src/plone/restapi/slots/handlers.py @@ -16,9 +16,9 @@ def handle_block_removed_event(event): slots = ISlots(obj) for slot in slots.values(): - if blockid in slot['slot_blocks_layout']['items']: - slot['slot_blocks_layout']['items'] = [ - bid for bid in slot['slot_blocks_layout']['items'] + if blockid in slot['blocks_layout']['items']: + slot['blocks_layout']['items'] = [ + bid for bid in slot['blocks_layout']['items'] if bid != blockid ] slot._p_changed = True @@ -42,9 +42,9 @@ def handle_blocks_removed_event(event): slots = ISlots(obj) for slot in slots.values(): - if set_block_ids.intersection(slot['slot_blocks_layout']['items']): - slot['slot_blocks_layout']['items'] = [ - bid for bid in slot['slot_blocks_layout']['items'] + if set_block_ids.intersection(slot['blocks_layout']['items']): + slot['blocks_layout']['items'] = [ + bid for bid in slot['blocks_layout']['items'] if bid not in blockids ] slot._p_changed = True diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index f7f35dd452..0cb6c4484e 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -45,29 +45,29 @@ def test_slot_empty(self): res = self.serialize(self.portal, storage['left']) self.assertEqual(res, { '@id': 'http://nohost/plone/@slots/left', - 'slot_blocks': {}, - 'slot_blocks_layout': {'items': []} + 'blocks': {}, + 'blocks_layout': {'items': []} }) def test_slot(self): storage = ISlotStorage(self.portal) storage['left'] = Slot(**({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} + 'blocks': {1: {}, 2: {}, 3: {}, }, + 'blocks_layout': {'items': [1, 2, 3]} })) res = self.serialize(self.portal, storage['left']) self.assertEqual(res, { '@id': 'http://nohost/plone/@slots/left', - 'slot_blocks_layout': {'items': [1, 2, 3]}, - 'slot_blocks': {1: {}, 2: {}, 3: {}} + 'blocks_layout': {'items': [1, 2, 3]}, + 'blocks': {1: {}, 2: {}, 3: {}} }) def test_slot_deep(self): rootstore = ISlotStorage(self.portal) rootstore['left'] = Slot(**({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} + 'blocks': {1: {}, 2: {}, 3: {}, }, + 'blocks_layout': {'items': [1, 2, 3]} })) storage = ISlotStorage(self.doc) @@ -75,80 +75,80 @@ def test_slot_deep(self): res = self.serialize(self.doc, storage['left']) self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'slot_blocks': {1: {u'_v_inherit': True}, + 'blocks': {1: {u'_v_inherit': True}, 2: {u'_v_inherit': True}, 3: {u'_v_inherit': True}}, - 'slot_blocks_layout': {'items': [1, 2, 3]}}) + 'blocks_layout': {'items': [1, 2, 3]}}) def test_data_override_with_isVariant(self): rootstore = ISlotStorage(self.portal) rootstore['left'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 1: {'title': 'First'}, 3: {'title': 'Third'}, }, - 'slot_blocks_layout': {'items': [1, 3]} + 'blocks_layout': {'items': [1, 3]} })) storage = ISlotStorage(self.doc) storage['left'] = Slot( - slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - slot_blocks_layout={'items': [2]}, + blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + blocks_layout={'items': [2]}, ) res = self.serialize(self.doc, storage['left']) self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'slot_blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, + 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, 3: {u'_v_inherit': True, u'title': u'Third'}}, - 'slot_blocks_layout': {'items': [2, 3]}}) + 'blocks_layout': {'items': [2, 3]}}) def test_change_order_from_layout(self): rootstore = ISlotStorage(self.portal) rootstore['left'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 1: {'title': 'First'}, 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, - 'slot_blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [5, 1, 3]} })) storage = ISlotStorage(self.doc) storage['left'] = Slot( - slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - slot_blocks_layout={'items': [3, 2]}, + blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + blocks_layout={'items': [3, 2]}, ) res = self.serialize(self.doc, storage['left']) self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'slot_blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, + 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, 3: {u'_v_inherit': True, u'title': u'Third'}, 5: {u'_v_inherit': True, u'title': u'Fifth'}}, - 'slot_blocks_layout': {'items': [3, 2, 5]}}) + 'blocks_layout': {'items': [3, 2, 5]}}) def test_serialize_storage(self): rootstore = ISlotStorage(self.portal) rootstore['left'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 1: {'title': 'First'}, 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, - 'slot_blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [5, 1, 3]} })) rootstore['right'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 6: {'title': 'First'}, 7: {'title': 'Third'}, 8: {'title': 'Fifth'}, }, - 'slot_blocks_layout': {'items': [8, 6, 7]} + 'blocks_layout': {'items': [8, 6, 7]} })) storage = ISlotStorage(self.doc) storage['left'] = Slot( - slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - slot_blocks_layout={'items': [3, 2]}, + blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + blocks_layout={'items': [3, 2]}, ) res = self.serialize(self.doc, storage) @@ -157,17 +157,17 @@ def test_serialize_storage(self): 'items': { u'left': { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'slot_blocks': { + 'blocks': { 2: {u's:isVariantOf': 1, u'title': u'Second'}, 3: {u'_v_inherit': True, u'title': u'Third'}, 5: {u'_v_inherit': True, u'title': u'Fifth'} }, - 'slot_blocks_layout': {'items': [3, 2, 5]}}, + 'blocks_layout': {'items': [3, 2, 5]}}, u'right': { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/right', - 'slot_blocks': { + 'blocks': { 6: {u'title': u'First', u'_v_inherit': True}, 7: {u'title': u'Third', u'_v_inherit': True}, 8: {u'title': u'Fifth', u'_v_inherit': True} }, - 'slot_blocks_layout': {'items': [8, 6, 7]}}}}) + 'blocks_layout': {'items': [8, 6, 7]}}}}) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 51e19dcc39..4439cecf93 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -82,26 +82,26 @@ def populateSite(self): rootstore = ISlotStorage(self.portal) rootstore['left'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 1: {'title': 'First'}, 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, - 'slot_blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [5, 1, 3]} })) rootstore['right'] = Slot(**({ - 'slot_blocks': { + 'blocks': { 6: {'title': 'First'}, 7: {'title': 'Third'}, 8: {'title': 'Fifth'}, }, - 'slot_blocks_layout': {'items': [8, 6, 7]} + 'blocks_layout': {'items': [8, 6, 7]} })) storage = ISlotStorage(self.doc) storage['left'] = Slot( - slot_blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - slot_blocks_layout={'items': [3, 2]}, + blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, + blocks_layout={'items': [3, 2]}, ) setRoles(self.portal, TEST_USER_ID, ["Member"]) @@ -113,15 +113,15 @@ def test_slots_endpoint(self): u'@id': u'http://localhost:55001/plone/@slots', u'edit_slots': [u'right', u'left'], u'items': {u'left': {u'@id': u'http://localhost:55001/plone/@slots/left', - u'slot_blocks': {u'1': {u'title': u'First'}, + u'blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, - u'slot_blocks_layout': {u'items': [5, 1, 3]}}, + u'blocks_layout': {u'items': [5, 1, 3]}}, u'right': {u'@id': u'http://localhost:55001/plone/@slots/right', - u'slot_blocks': {u'6': {u'title': u'First'}, + u'blocks': {u'6': {u'title': u'First'}, u'7': {u'title': u'Third'}, u'8': {u'title': u'Fifth'}}, - u'slot_blocks_layout': {u'items': [8, 6, 7]}}}} + u'blocks_layout': {u'items': [8, 6, 7]}}}} ) def test_slot_endpoint(self): @@ -134,10 +134,10 @@ def test_slot_endpoint_on_root(self): self.assertEqual(response.json(), { u'@id': u'http://localhost:55001/plone/@slots/left', u'edit': True, - u'slot_blocks': {u'1': {u'title': u'First'}, + u'blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, - u'slot_blocks_layout': {u'items': [5, 1, 3]}}) + u'blocks_layout': {u'items': [5, 1, 3]}}) # def test_deserializer_slot_not_found(self): # response = self.api_session.patch('/@slots/left', json={}) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 178cc408b8..70d8af14fc 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -63,14 +63,14 @@ def get(self, name): class DummySlot(object): def __init__(self, data={}): - self.slot_blocks_layout = data and data['slot_blocks_layout'] or {"items": []} - self.slot_blocks = data and data['slot_blocks'] or {} + self.blocks_layout = data and data['blocks_layout'] or {"items": []} + self.blocks = data and data['blocks'] or {} @classmethod def from_data(cls, blocks, layout): res = { - 'slot_blocks_layout': {"items": layout}, - 'slot_blocks': blocks + 'blocks_layout': {"items": layout}, + 'blocks': blocks } return cls(res) @@ -98,8 +98,8 @@ def test_slot_stack_on_root(self): # simple test with one level stack of slots root = self.make_content() root.slots['left'] = DummySlot({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} + 'blocks': {1: {}, 2: {}, 3: {}, }, + 'blocks_layout': {'items': [1, 2, 3]} }) root.slots['right'] = DummySlot() engine = Slots(root) @@ -108,20 +108,20 @@ def test_slot_stack_on_root(self): right_stack = engine.get_fills_stack('right') self.assertEqual(len(right_stack), 1) - self.assertEqual(right_stack[0].slot_blocks_layout, {"items": []}) - self.assertEqual(right_stack[0].slot_blocks, {}) + self.assertEqual(right_stack[0].blocks_layout, {"items": []}) + self.assertEqual(right_stack[0].blocks, {}) left_stack = engine.get_fills_stack('left') self.assertEqual(len(left_stack), 1) - self.assertEqual(left_stack[0].slot_blocks_layout, {'items': [1, 2, 3]}) - self.assertEqual(left_stack[0].slot_blocks, {1: {}, 2: {}, 3: {}}) + self.assertEqual(left_stack[0].blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left_stack[0].blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep(self): # the slot stack is inherited further down root = self.make_content() root.slots['left'] = DummySlot({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} + 'blocks': {1: {}, 2: {}, 3: {}, }, + 'blocks_layout': {'items': [1, 2, 3]} }) engine = Slots(root['documents']['internal']['company-a']) @@ -132,15 +132,15 @@ def test_slot_stack_deep(self): self.assertEqual(len(left_stack), 4) left = left_stack[3] - self.assertEqual(left.slot_blocks_layout, {'items': [1, 2, 3]}) - self.assertEqual(left.slot_blocks, {1: {}, 2: {}, 3: {}}) + self.assertEqual(left.blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left.blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_data_in_root(self): # slots stacks up from deepest to shallow root = self.make_content() root.slots['left'] = DummySlot({ - 'slot_blocks': {1: {}, 2: {}, 3: {}, }, - 'slot_blocks_layout': {'items': [1, 2, 3]} + 'blocks': {1: {}, 2: {}, 3: {}, }, + 'blocks_layout': {'items': [1, 2, 3]} }) obj = root['documents']['internal']['company-a'] @@ -154,12 +154,12 @@ def test_slot_stack_deep_with_data_in_root(self): self.assertEqual(stack[1:3], [None, None]) first = stack[0] - self.assertEqual(first.slot_blocks_layout, {'items': [4, 5, 6]}) - self.assertEqual(first.slot_blocks, {4: {}, 5: {}, 6: {}}) + self.assertEqual(first.blocks_layout, {'items': [4, 5, 6]}) + self.assertEqual(first.blocks, {4: {}, 5: {}, 6: {}}) last = stack[3] - self.assertEqual(last.slot_blocks_layout, {'items': [1, 2, 3]}) - self.assertEqual(last.slot_blocks, {1: {}, 2: {}, 3: {}}) + self.assertEqual(last.blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(last.blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_stack_collapse(self): # get_blocks collapses the stack and marks inherited slots with _v_inherit @@ -179,8 +179,8 @@ def test_slot_stack_deep_with_stack_collapse(self): left = engine.get_blocks('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [4, 5, 6, 7, 1, 2, 3]}, - 'slot_blocks': { + 'blocks_layout': {'items': [4, 5, 6, 7, 1, 2, 3]}, + 'blocks': { 4: {}, 5: {}, 6: {}, @@ -205,8 +205,8 @@ def test_block_data_gets_inherited(self): left = engine.get_blocks('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [2, 1]}, - 'slot_blocks': { + 'blocks_layout': {'items': [2, 1]}, + 'blocks': { 1: {'title': 'First', '_v_inherit': True}, 2: {} } @@ -231,8 +231,8 @@ def test_block_data_gets_override(self): left = engine.get_blocks('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [2, 3]}, - 'slot_blocks': { + 'blocks_layout': {'items': [2, 3]}, + 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, } @@ -260,8 +260,8 @@ def test_can_change_order_with_sameOf(self): left = engine.get_blocks('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [4, 2, 5]}, - 'slot_blocks': { + 'blocks_layout': {'items': [4, 2, 5]}, + 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 4: {'title': 'Third', 's:sameAs': 3, '_v_inherit': True}, 5: {'title': 'Fifth', '_v_inherit': True}, @@ -289,8 +289,8 @@ def test_can_change_order_from_layout(self): left = engine.get_blocks('left') self.assertEqual(left, { - 'slot_blocks_layout': {'items': [3, 2, 5]}, - 'slot_blocks': { + 'blocks_layout': {'items': [3, 2, 5]}, + 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, 5: {'title': 'Fifth', '_v_inherit': True}, @@ -299,8 +299,8 @@ def test_can_change_order_from_layout(self): def test_save_slots(self): data = { - 'slot_blocks_layout': {'items': [3, 2, 5]}, - 'slot_blocks': { + 'blocks_layout': {'items': [3, 2, 5]}, + 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, 5: {'title': 'Fifth', '_v_inherit': True}, @@ -314,8 +314,8 @@ def test_save_slots(self): slot = DummySlot() engine.save_data_to_slot(slot, data) - self.assertEqual(slot.slot_blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) - self.assertEqual(slot.slot_blocks_layout, {'items': [3, 2, 5]}) + self.assertEqual(slot.blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) + self.assertEqual(slot.blocks_layout, {'items': [3, 2, 5]}) class TestSlotsStorage(unittest.TestCase): @@ -455,8 +455,8 @@ def test_deserialize_put_one(self): deserializer = getMultiAdapter( (self.doc, storage, self.request), IDeserializeFromJson) deserializer({"left": { - 'slot_blocks_layout': {'items': [3, 2, 5]}, - 'slot_blocks': { + 'blocks_layout': {'items': [3, 2, 5]}, + 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, 5: {'title': 'Fifth', '_v_inherit': True}, @@ -464,5 +464,3 @@ def test_deserialize_put_one(self): }}) self.assertEqual(list(storage.keys()), ['left']) - import pdb - pdb.set_trace() From 895f5ad7544cb3edcc90d55c34db531df350e398 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 09:55:16 +0200 Subject: [PATCH 32/99] Visual formatting --- src/plone/restapi/tests/test_services_slots.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 4439cecf93..a741b0ac31 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -114,13 +114,13 @@ def test_slots_endpoint(self): u'edit_slots': [u'right', u'left'], u'items': {u'left': {u'@id': u'http://localhost:55001/plone/@slots/left', u'blocks': {u'1': {u'title': u'First'}, - u'3': {u'title': u'Third'}, - u'5': {u'title': u'Fifth'}}, + u'3': {u'title': u'Third'}, + u'5': {u'title': u'Fifth'}}, u'blocks_layout': {u'items': [5, 1, 3]}}, u'right': {u'@id': u'http://localhost:55001/plone/@slots/right', u'blocks': {u'6': {u'title': u'First'}, - u'7': {u'title': u'Third'}, - u'8': {u'title': u'Fifth'}}, + u'7': {u'title': u'Third'}, + u'8': {u'title': u'Fifth'}}, u'blocks_layout': {u'items': [8, 6, 7]}}}} ) @@ -135,8 +135,8 @@ def test_slot_endpoint_on_root(self): u'@id': u'http://localhost:55001/plone/@slots/left', u'edit': True, u'blocks': {u'1': {u'title': u'First'}, - u'3': {u'title': u'Third'}, - u'5': {u'title': u'Fifth'}}, + u'3': {u'title': u'Third'}, + u'5': {u'title': u'Fifth'}}, u'blocks_layout': {u'items': [5, 1, 3]}}) # def test_deserializer_slot_not_found(self): From 15305b3c241831e19359d16eb134617cf7e3bc76 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 09:56:15 +0200 Subject: [PATCH 33/99] Visual formatting --- src/plone/restapi/tests/test_serializer_slots.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index 0cb6c4484e..0a1a3b2e33 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -76,8 +76,8 @@ def test_slot_deep(self): self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', 'blocks': {1: {u'_v_inherit': True}, - 2: {u'_v_inherit': True}, - 3: {u'_v_inherit': True}}, + 2: {u'_v_inherit': True}, + 3: {u'_v_inherit': True}}, 'blocks_layout': {'items': [1, 2, 3]}}) def test_data_override_with_isVariant(self): @@ -99,7 +99,7 @@ def test_data_override_with_isVariant(self): self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, - 3: {u'_v_inherit': True, u'title': u'Third'}}, + 3: {u'_v_inherit': True, u'title': u'Third'}}, 'blocks_layout': {'items': [2, 3]}}) def test_change_order_from_layout(self): @@ -122,8 +122,8 @@ def test_change_order_from_layout(self): self.assertEqual(res, { '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, - 3: {u'_v_inherit': True, u'title': u'Third'}, - 5: {u'_v_inherit': True, u'title': u'Fifth'}}, + 3: {u'_v_inherit': True, u'title': u'Third'}, + 5: {u'_v_inherit': True, u'title': u'Fifth'}}, 'blocks_layout': {'items': [3, 2, 5]}}) def test_serialize_storage(self): From cdc54c3d601fbeadd4850dbe984ebfe93ab42194 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 10:38:25 +0200 Subject: [PATCH 34/99] Move interfaces to .slots package --- src/plone/restapi/deserializer/slots.py | 4 +- src/plone/restapi/indexers.py | 2 +- src/plone/restapi/interfaces.py | 41 ------------------- src/plone/restapi/serializer/slots.py | 6 +-- src/plone/restapi/services/slots/get.py | 4 +- src/plone/restapi/services/slots/update.py | 2 +- src/plone/restapi/slots/__init__.py | 6 +-- src/plone/restapi/slots/configure.zcml | 4 +- src/plone/restapi/slots/handlers.py | 2 +- src/plone/restapi/slots/interfaces.py | 13 ++++++ .../restapi/tests/test_serializer_slots.py | 2 +- .../restapi/tests/test_services_slots.py | 2 +- src/plone/restapi/tests/test_slots.py | 8 ++-- 13 files changed, 34 insertions(+), 62 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 1fa05b67dc..c9b8d5497b 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -6,9 +6,9 @@ from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson -from plone.restapi.interfaces import ISlot -from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlot +from plone.restapi.slots.interfaces import ISlotStorage from Products.CMFCore.interfaces import IContentish from Products.CMFPlone.interfaces import IPloneSiteRoot from zope.component import adapter diff --git a/src/plone/restapi/indexers.py b/src/plone/restapi/indexers.py index 105cc0737e..4b1dfd1bfd 100644 --- a/src/plone/restapi/indexers.py +++ b/src/plone/restapi/indexers.py @@ -10,8 +10,8 @@ from plone.indexer.decorator import indexer from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockSearchableText -from plone.restapi.interfaces import ISlots from plone.restapi.slots import SLOTS_KEY +from plone.restapi.slots.interfaces import ISlots from Products.CMFCore.interfaces import IContentish from zope.annotation.interfaces import IAnnotations from zope.component import adapter diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 51ff499839..f5540d60c7 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -4,14 +4,11 @@ # E0211: Method has no argument # W0221: Arguments number differs from overridden '__call__' method -from plone.schema import JSONField from zope.interface import Attribute from zope.interface import Interface from zope.interface.interfaces import IObjectEvent from zope.publisher.interfaces.browser import IDefaultBrowserLayer -import json - class IPloneRestapiLayer(IDefaultBrowserLayer): """Marker interface that defines a browser layer.""" @@ -216,44 +213,6 @@ def __call__(value): """Extract text from the block value. Returns text""" -class ISlots(Interface): - """Slots are named container of sets of blocks""" - - -class ISlotStorage(Interface): - """ A store of slots information """ - - -SLOT_BLOCKS_SCHEMA = json.dumps({"type": "object", "properties": {}}) - -SLOT_LAYOUT_SCHEMA = json.dumps( - { - "type": "object", - "properties": {"items": {"type": "array", "items": {"type": "string"}}}, - } -) - - -class ISlot(Interface): - """Slots follow the IBlocks model""" - - blocks = JSONField( - title=u"Slot blocks", - description=u"The JSON representation of the slot blocks information. Must be a JSON object.", # noqa - schema=SLOT_BLOCKS_SCHEMA, - default={}, - required=False, - ) - - blocks_layout = JSONField( - title=u"Slot blocks Layout", - description=u"The JSON representation of the slot blocks layout. Must be a JSON array.", # noqa - schema=SLOT_LAYOUT_SCHEMA, - default={"items": []}, - required=False, - ) - - class IBlocksRemovedEvent(IObjectEvent): """ A bunch of blocks have been removed """ diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 558425c7fc..c13545019c 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.interfaces import ISlot -from plone.restapi.interfaces import ISlots -from plone.restapi.interfaces import ISlotStorage from plone.restapi.serializer.converters import json_compatible from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlot +from plone.restapi.slots.interfaces import ISlots +from plone.restapi.slots.interfaces import ISlotStorage from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index afca3efebc..933651f7a4 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.interfaces import ISlots -from plone.restapi.interfaces import ISlotStorage from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlots +from plone.restapi.slots.interfaces import ISlotStorage from zope.component import getMultiAdapter from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index 64d75004a2..d1b69666c9 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -6,9 +6,9 @@ from plone.restapi.exceptions import DeserializationError from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.interfaces import ISlots from plone.restapi.services import Service from plone.restapi.services.locking.locking import is_locked +from plone.restapi.slots.interfaces import ISlots from zope.component import getMultiAdapter from zope.event import notify from zope.lifecycleevent import ObjectModifiedEvent diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 2e74385210..44b3a2f682 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- +from .interfaces import ISlot +from .interfaces import ISlots +from .interfaces import ISlotStorage from AccessControl.SecurityManagement import getSecurityManager from copy import deepcopy from persistent import Persistent from plone.registry.interfaces import IRegistry -from plone.restapi.interfaces import ISlot -from plone.restapi.interfaces import ISlots -from plone.restapi.interfaces import ISlotStorage from plone.restapi.permissions import ModifySlotsPermission from plone.restapi.slots.interfaces import ISlotSettings from Products.CMFCore.interfaces import IContentish diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index 81a23d0c93..ea95a2b0af 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -11,13 +11,13 @@ diff --git a/src/plone/restapi/slots/handlers.py b/src/plone/restapi/slots/handlers.py index 07393df34b..d3620dc7af 100644 --- a/src/plone/restapi/slots/handlers.py +++ b/src/plone/restapi/slots/handlers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from plone.api import portal -from plone.restapi.interfaces import ISlots +from plone.restapi.slots.interfaces import ISlots def handle_block_removed_event(event): diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index ab2c57266f..fd98ba4626 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -1,10 +1,23 @@ # -*- coding: utf-8 -*- +from plone.restapi.behaviors import IBlocks from zope.interface import Interface from zope.schema import List from zope.schema import TextLine +class ISlots(Interface): + """Slots are named container of sets of blocks""" + + +class ISlotStorage(Interface): + """ A store of slots information """ + + +class ISlot(IBlocks): + """Slots follow the IBlocks model""" + + class ISlotSettings(Interface): content_slots = List( title=u"Content slots", diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index 0a1a3b2e33..c6477578be 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -3,8 +3,8 @@ # from plone.app.testing import TEST_USER_ID from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from zope.component import getMultiAdapter diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index a741b0ac31..297a9c03aa 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -4,8 +4,8 @@ from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_ID -from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import RelativeSession diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 70d8af14fc..c301f596bb 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -3,11 +3,11 @@ from plone.api import portal from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import IDeserializeFromJson -from plone.restapi.interfaces import ISlots -from plone.restapi.interfaces import ISlotStorage from plone.restapi.slots import Slot from plone.restapi.slots import Slots +from plone.restapi.slots.interfaces import ISlots from plone.restapi.slots.interfaces import ISlotSettings +from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase from zope.component import getMultiAdapter @@ -445,7 +445,7 @@ def test_editable_slots_as_editor(self): def test_deserialize_empty(self): storage = ISlotStorage(self.doc) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) + (self.doc, storage, self.request), IDeserializeFromJson) deserializer() self.assertEqual(list(storage.keys()), []) @@ -453,7 +453,7 @@ def test_deserialize_empty(self): def test_deserialize_put_one(self): storage = ISlotStorage(self.doc) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) + (self.doc, storage, self.request), IDeserializeFromJson) deserializer({"left": { 'blocks_layout': {'items': [3, 2, 5]}, 'blocks': { From b4693a3f1096c67fd0a6ae5c2348214a239261c3 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 12:24:47 +0200 Subject: [PATCH 35/99] Separate deserializer test --- .../restapi/tests/test_deserializer_slots.py | 70 +++++++++++++++++++ src/plone/restapi/tests/test_slots.py | 25 ------- 2 files changed, 70 insertions(+), 25 deletions(-) create mode 100644 src/plone/restapi/tests/test_deserializer_slots.py diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py new file mode 100644 index 0000000000..c4cf6aebb5 --- /dev/null +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -0,0 +1,70 @@ +from plone.dexterity.utils import createContentInContainer +from plone.restapi.interfaces import IDeserializeFromJson +from plone.restapi.slots.interfaces import ISlotStorage +from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING +from Products.CMFPlone.tests.PloneTestCase import PloneTestCase +from zope.component import getMultiAdapter + + +# from plone.api import portal +# from plone.restapi.slots import Slot +# from plone.restapi.slots import Slots +# from plone.restapi.slots.interfaces import ISlots +# from plone.restapi.slots.interfaces import ISlotSettings +# from zope.component import provideAdapter +# from zope.interface import implements +# from zope.interface import Interface + + +class TestSlotsEngineIntegration(PloneTestCase): + + layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING + + def setUp(self): + self.app = self.layer["app"] + self.portal = self.layer["portal"] + self.request = self.layer["request"] + + self.portal.acl_users.userFolderAddUser( + 'simple_member', 'slots_pw', ["Member"], [] + ) + + self.portal.acl_users.userFolderAddUser( + 'editor_member', 'slots_pw', ["Editor"], [] + ) + + self.make_content() + + def make_content(self): + self.documents = createContentInContainer( + self.portal, u"Folder", id=u"documents", title=u"Documents" + ) + self.company = createContentInContainer( + self.documents, u"Folder", id=u"company-a", title=u"Documents" + ) + self.doc = createContentInContainer( + self.company, u"Document", id=u"doc-1", title=u"Doc 1" + ) + + def test_deserialize_empty(self): + storage = ISlotStorage(self.doc) + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + deserializer() + + self.assertEqual(list(storage.keys()), []) + + def test_deserialize_put_one(self): + storage = ISlotStorage(self.doc) + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + deserializer({"left": { + 'blocks_layout': {'items': [3, 2, 5]}, + 'blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + } + }}) + + self.assertEqual(list(storage.keys()), ['left']) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index c301f596bb..4736a15e78 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -2,7 +2,6 @@ from plone.api import portal from plone.dexterity.utils import createContentInContainer -from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.slots import Slot from plone.restapi.slots import Slots from plone.restapi.slots.interfaces import ISlots @@ -10,7 +9,6 @@ from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase -from zope.component import getMultiAdapter from zope.component import provideAdapter from zope.interface import implements from zope.interface import Interface @@ -441,26 +439,3 @@ def test_editable_slots_as_editor(self): proxy.content_slots = [u'left'] self.assertEqual(engine.get_editable_slots(), [u'left']) - - def test_deserialize_empty(self): - storage = ISlotStorage(self.doc) - deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) - deserializer() - - self.assertEqual(list(storage.keys()), []) - - def test_deserialize_put_one(self): - storage = ISlotStorage(self.doc) - deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) - deserializer({"left": { - 'blocks_layout': {'items': [3, 2, 5]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, - } - }}) - - self.assertEqual(list(storage.keys()), ['left']) From 15809aad44c89094cd6860952e1657b55d608de0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 15:25:08 +0200 Subject: [PATCH 36/99] Fix deserializer --- src/plone/restapi/deserializer/blocks.py | 2 +- src/plone/restapi/deserializer/slots.py | 7 ++++++- .../restapi/tests/test_deserializer_slots.py | 17 ++++++----------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index 68a6a103a8..a24369d0ac 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -196,7 +196,7 @@ def __init__(self, context, request): def __call__(self, block): for k, v in block.items(): if k.startswith('_v_'): - del v[k] + del block[k] return block diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index c9b8d5497b..256b1356ca 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -45,12 +45,17 @@ def __call__(self, data=None): notify(BlocksRemovedEvent(dict(context=self.context, blocks=removed_blocks))) + # we don't want to store blocks that are inherited + for k, v in blocks.items(): + if v.get('_v_inherit'): + del blocks[k] + for id, block_value in blocks.items(): block_type = block_value.get("@type", "") handlers = [] for h in subscribers( - (self.context, self.request), + (self.slot, self.request), IBlockFieldDeserializationTransformer, ): if h.block_type == block_type or h.block_type is None: diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index c4cf6aebb5..ea95c3bef8 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -6,16 +6,6 @@ from zope.component import getMultiAdapter -# from plone.api import portal -# from plone.restapi.slots import Slot -# from plone.restapi.slots import Slots -# from plone.restapi.slots.interfaces import ISlots -# from plone.restapi.slots.interfaces import ISlotSettings -# from zope.component import provideAdapter -# from zope.interface import implements -# from zope.interface import Interface - - class TestSlotsEngineIntegration(PloneTestCase): layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING @@ -56,10 +46,12 @@ def test_deserialize_empty(self): def test_deserialize_put_one(self): storage = ISlotStorage(self.doc) + deserializer = getMultiAdapter( (self.doc, storage, self.request), IDeserializeFromJson) + deserializer({"left": { - 'blocks_layout': {'items': [3, 2, 5]}, + 'blocks_layout': {'items': [3, 2, 5, 4]}, 'blocks': { 2: {'title': 'Second', 's:isVariantOf': 1}, 3: {'title': 'Third', '_v_inherit': True}, @@ -68,3 +60,6 @@ def test_deserialize_put_one(self): }}) self.assertEqual(list(storage.keys()), ['left']) + self.assertEqual(storage['left'].blocks, + {2: {'title': 'Second', 's:isVariantOf': 1}, }) + self.assertEqual(storage['left'].blocks_layout, {"items": [3, 2, 5, 4]}) From 3505931196392511bff7eace3f4b3819d3477df2 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 15:25:35 +0200 Subject: [PATCH 37/99] Fix deserializer --- src/plone/restapi/tests/test_deserializer_slots.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index ea95c3bef8..b0db816485 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.slots.interfaces import ISlotStorage From b70875e8d970dd51d781051236c78f9cb98d0789 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 17:09:23 +0200 Subject: [PATCH 38/99] Fix deserializer --- src/plone/restapi/deserializer/slots.py | 8 ++- .../restapi/tests/test_deserializer_slots.py | 57 ++++++++++++++++--- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 256b1356ca..60741c80c5 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -35,6 +35,9 @@ def __call__(self, data=None): if data is None: data = json_body(self.request) + if not data: + return + blocks = copy.deepcopy(data['blocks']) existing_blocks = self.slot.blocks @@ -95,13 +98,14 @@ def __call__(self, data=None): for name, slot in self.storage.items(): slotdata = data.get(name, None) - if slotdata is None: + if not slotdata: notify(BlocksRemovedEvent(dict( context=self.context, blocks=slot.blocks ))) - del self.storage[name] + self.storage[name].blocks = {} + self.storage[name].blocks_layout = {"items": []} for name, slotdata in data.items(): if name not in self.storage: diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index b0db816485..6fe524ed57 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -2,6 +2,7 @@ from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import IDeserializeFromJson +from plone.restapi.slots import Slot from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase @@ -46,22 +47,60 @@ def test_deserialize_empty(self): self.assertEqual(list(storage.keys()), []) - def test_deserialize_put_one(self): + def test_deserialize_put_some(self): storage = ISlotStorage(self.doc) deserializer = getMultiAdapter( (self.doc, storage, self.request), IDeserializeFromJson) - deserializer({"left": { - 'blocks_layout': {'items': [3, 2, 5, 4]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, + deserializer({ + "left": { + 'blocks_layout': {'items': [3, 2, 5, 4]}, + 'blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + }, + }, + "right": { + 'blocks_layout': {'items': [6, 7]}, + 'blocks': { + 6: {'title': 'Sixth'}, + } } - }}) + }) - self.assertEqual(list(storage.keys()), ['left']) + self.assertEqual(list(storage.keys()), ['left', 'right']) self.assertEqual(storage['left'].blocks, {2: {'title': 'Second', 's:isVariantOf': 1}, }) self.assertEqual(storage['left'].blocks_layout, {"items": [3, 2, 5, 4]}) + + self.assertEqual(storage['right'].blocks, + {6: {'title': 'Sixth'}, }) + self.assertEqual(storage['right'].blocks_layout, {"items": [6, 7]}) + + def test_delete_all(self): + storage = ISlotStorage(self.doc) + storage['left'] = Slot(**({ + 'blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + })) + storage['right'] = Slot(**({ + 'blocks': { + 6: {'title': 'First'}, + 7: {'title': 'Third'}, + 8: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [8, 6, 7]} + })) + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + deserializer({'left': {}, 'right': {}}) + + left = storage['left'] + self.assertEqual(left.blocks, {}) + self.assertEqual(left.blocks_layout, {"items": []}) From e23c68d64e8c723ff8f53fdbe00a77fec519d0cc Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 17:49:20 +0200 Subject: [PATCH 39/99] Add another test --- .../restapi/tests/test_deserializer_slots.py | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 6fe524ed57..5e3d303ee3 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -79,8 +79,9 @@ def test_deserialize_put_some(self): {6: {'title': 'Sixth'}, }) self.assertEqual(storage['right'].blocks_layout, {"items": [6, 7]}) - def test_delete_all(self): + def test_delete_all_with_dict(self): storage = ISlotStorage(self.doc) + storage['left'] = Slot(**({ 'blocks': { 1: {'title': 'First'}, @@ -97,6 +98,7 @@ def test_delete_all(self): }, 'blocks_layout': {'items': [8, 6, 7]} })) + deserializer = getMultiAdapter( (self.doc, storage, self.request), IDeserializeFromJson) deserializer({'left': {}, 'right': {}}) @@ -104,3 +106,32 @@ def test_delete_all(self): left = storage['left'] self.assertEqual(left.blocks, {}) self.assertEqual(left.blocks_layout, {"items": []}) + + def test_delete_all_with_empty(self): + storage = ISlotStorage(self.doc) + + storage['left'] = Slot(**({ + 'blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + })) + storage['right'] = Slot(**({ + 'blocks': { + 6: {'title': 'First'}, + 7: {'title': 'Third'}, + 8: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [8, 6, 7]} + })) + + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + + deserializer({}) + + left = storage['left'] + self.assertEqual(left.blocks, {}) + self.assertEqual(left.blocks_layout, {"items": []}) From afae726e2324615e122aa5cd2577822e2950eaea Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 21:35:28 +0200 Subject: [PATCH 40/99] Fix indexing, test remove of block from child layout --- src/plone/restapi/indexers.py | 17 ---- src/plone/restapi/indexers.zcml | 5 -- src/plone/restapi/slots/configure.zcml | 5 ++ src/plone/restapi/slots/handlers.py | 19 +++-- src/plone/restapi/slots/indexers.py | 18 +++++ .../restapi/tests/test_deserializer_slots.py | 77 +++++++++++++++++++ 6 files changed, 109 insertions(+), 32 deletions(-) create mode 100644 src/plone/restapi/slots/indexers.py diff --git a/src/plone/restapi/indexers.py b/src/plone/restapi/indexers.py index 4b1dfd1bfd..588f126b87 100644 --- a/src/plone/restapi/indexers.py +++ b/src/plone/restapi/indexers.py @@ -10,10 +10,6 @@ from plone.indexer.decorator import indexer from plone.restapi.behaviors import IBlocks from plone.restapi.interfaces import IBlockSearchableText -from plone.restapi.slots import SLOTS_KEY -from plone.restapi.slots.interfaces import ISlots -from Products.CMFCore.interfaces import IContentish -from zope.annotation.interfaces import IAnnotations from zope.component import adapter from zope.component import queryMultiAdapter from zope.globalrequest import getRequest @@ -77,16 +73,3 @@ def SearchableText_blocks(obj): blocks_text.append(std_text) return " ".join(blocks_text) - - -@indexer(IContentish) -def slot_block_ids(obj): - if SLOTS_KEY not in IAnnotations(obj): - return - - blocks = [] - storage = ISlots(obj) - for name, slot in storage.items(): - blocks.extend(slot.blocks_layout['items']) - - return blocks diff --git a/src/plone/restapi/indexers.zcml b/src/plone/restapi/indexers.zcml index 5f47f838e6..b9f1e74960 100644 --- a/src/plone/restapi/indexers.zcml +++ b/src/plone/restapi/indexers.zcml @@ -15,9 +15,4 @@ /> - - diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index ea95a2b0af..ddbd59799a 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -51,4 +51,9 @@ name="slots" /> + + diff --git a/src/plone/restapi/slots/handlers.py b/src/plone/restapi/slots/handlers.py index d3620dc7af..a9895cc5ed 100644 --- a/src/plone/restapi/slots/handlers.py +++ b/src/plone/restapi/slots/handlers.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from plone.api import portal -from plone.restapi.slots.interfaces import ISlots +from plone.restapi.slots.interfaces import ISlotStorage def handle_block_removed_event(event): - # TODO: needs rewrite info = event.object catalog = portal.get_tool('portal_catalog') blockid = info['blockid'] @@ -14,11 +13,11 @@ def handle_block_removed_event(event): for brain in brains: obj = brain.getObject() - slots = ISlots(obj) + slots = ISlotStorage(obj) for slot in slots.values(): - if blockid in slot['blocks_layout']['items']: - slot['blocks_layout']['items'] = [ - bid for bid in slot['blocks_layout']['items'] + if blockid in slot.blocks_layout['items']: + slot.blocks_layout['items'] = [ + bid for bid in slot.blocks_layout['items'] if bid != blockid ] slot._p_changed = True @@ -40,11 +39,11 @@ def handle_blocks_removed_event(event): for brain in brains: obj = brain.getObject() - slots = ISlots(obj) + slots = ISlotStorage(obj) for slot in slots.values(): - if set_block_ids.intersection(slot['blocks_layout']['items']): - slot['blocks_layout']['items'] = [ - bid for bid in slot['blocks_layout']['items'] + if set_block_ids.intersection(slot.blocks_layout['items']): + slot.blocks_layout['items'] = [ + bid for bid in slot.blocks_layout['items'] if bid not in blockids ] slot._p_changed = True diff --git a/src/plone/restapi/slots/indexers.py b/src/plone/restapi/slots/indexers.py new file mode 100644 index 0000000000..7f51a52d56 --- /dev/null +++ b/src/plone/restapi/slots/indexers.py @@ -0,0 +1,18 @@ +from plone.indexer.decorator import indexer +from plone.restapi.slots import SLOTS_KEY +from plone.restapi.slots.interfaces import ISlotStorage +from Products.CMFCore.interfaces import IContentish +from zope.annotation.interfaces import IAnnotations + + +@indexer(IContentish) +def slot_block_ids(obj): + if SLOTS_KEY not in IAnnotations(obj): + return + + blocks = [] + storage = ISlotStorage(obj) + for name, slot in storage.items(): + blocks.extend(slot.blocks_layout['items']) + + return blocks diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 5e3d303ee3..4a76967f27 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -135,3 +135,80 @@ def test_delete_all_with_empty(self): left = storage['left'] self.assertEqual(left.blocks, {}) self.assertEqual(left.blocks_layout, {"items": []}) + + def test_delete_and_save(self): + storage = ISlotStorage(self.doc) + + storage['left'] = Slot(**({ + 'blocks': { + 1: {'title': 'First'}, + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + })) + storage['right'] = Slot(**({ + 'blocks': { + 6: {'title': 'First'}, + 7: {'title': 'Third'}, + 8: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [8, 6, 7]} + })) + + deserializer = getMultiAdapter( + (self.doc, storage, self.request), IDeserializeFromJson) + + deserializer({ + "left": { + 'blocks_layout': {'items': [3, 2, 5, 4]}, + 'blocks': { + 2: {'title': 'Second', 's:isVariantOf': 1}, + 3: {'title': 'Third', '_v_inherit': True}, + 5: {'title': 'Fifth', '_v_inherit': True}, + }, + }, + }) + + right = storage['right'] + self.assertEqual(right.blocks, {}) + self.assertEqual(right.blocks_layout, {"items": []}) + + left = storage['left'] + self.assertEqual(left.blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) + self.assertEqual(left.blocks_layout, {'items': [3, 2, 5, 4]}) + + def test_delete_in_parent_affects_child(self): + docstorage = ISlotStorage(self.doc) + + docstorage['left'] = Slot(**({ + 'blocks': { + 1: {'title': 'First'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + })) + + rootstorage = ISlotStorage(self.portal) + rootstorage['left'] = Slot(**({ + 'blocks': { + 3: {'title': 'Third'}, + 5: {'title': 'Fifth'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + })) + + self.portal.portal_catalog.indexObject(self.doc) + + deserializer = getMultiAdapter( + (self.doc, rootstorage, self.request), IDeserializeFromJson) + + deserializer({ + "left": { + 'blocks_layout': {'items': [3]}, + 'blocks': { + 3: {'title': 'Third', }, + }, + }, + }) + + self.assertEqual(docstorage['left'].blocks_layout['items'], [1, 3]) From 42f007d453511346978956b7ea00ef6e473f1406 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 21:35:53 +0200 Subject: [PATCH 41/99] Fix indexing, test remove of block from child layout --- src/plone/restapi/slots/indexers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plone/restapi/slots/indexers.py b/src/plone/restapi/slots/indexers.py index 7f51a52d56..1716f1b502 100644 --- a/src/plone/restapi/slots/indexers.py +++ b/src/plone/restapi/slots/indexers.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + from plone.indexer.decorator import indexer from plone.restapi.slots import SLOTS_KEY from plone.restapi.slots.interfaces import ISlotStorage From 6344b6401ca451be083e917221877ab20f204c6f Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 10 Feb 2021 23:35:49 +0200 Subject: [PATCH 42/99] WIP on deserializers --- src/plone/restapi/events.py | 6 ++--- src/plone/restapi/services/slots/update.py | 9 +++++--- .../restapi/tests/test_deserializer_slots.py | 2 +- .../restapi/tests/test_services_slots.py | 22 ++++++++++++++++--- src/plone/restapi/tests/test_slots.py | 4 ++-- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py index 03d8c5a882..5813c6010d 100644 --- a/src/plone/restapi/events.py +++ b/src/plone/restapi/events.py @@ -2,19 +2,19 @@ from plone.restapi.interfaces import IBlockRemovedEvent from plone.restapi.interfaces import IBlocksRemovedEvent -from zope.interface import implements +from zope.interface import implementer from zope.interface.interfaces import ObjectEvent +@implementer(IBlockRemovedEvent) class BlockRemovedEvent(ObjectEvent): - implements(IBlockRemovedEvent) def __init__(self, object): self.object = object +@implementer(IBlocksRemovedEvent) class BlocksRemovedEvent(ObjectEvent): - implements(IBlocksRemovedEvent) def __init__(self, object): self.object = object diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index d1b69666c9..26b5451494 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -8,12 +8,15 @@ from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from plone.restapi.services.locking.locking import is_locked -from plone.restapi.slots.interfaces import ISlots +from plone.restapi.slots.interfaces import ISlotStorage from zope.component import getMultiAdapter from zope.event import notify +from zope.interface import implementer from zope.lifecycleevent import ObjectModifiedEvent +from zope.publisher.interfaces import IPublishTraverse +@implementer(IPublishTraverse) class SlotsPatch(Service): """Update one or all the slots""" @@ -33,7 +36,7 @@ def reply(self): if self.params and len(self.params) > 0: return self.replySlot() - storage = ISlots(self.context) + storage = ISlotStorage(self.context) deserializer = getMultiAdapter( (self.context, storage, self.request), IDeserializeFromJson @@ -62,7 +65,7 @@ def reply(self): def replySlot(self): name = self.params[0] - storage = ISlots(self.context) + storage = ISlotStorage(self.context) slot = storage[name] deserializer = getMultiAdapter( diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 4a76967f27..80fdc2ef74 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -194,7 +194,7 @@ def test_delete_in_parent_affects_child(self): 3: {'title': 'Third'}, 5: {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [5, 3]} })) self.portal.portal_catalog.indexObject(self.doc) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 297a9c03aa..4f4ddc6499 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -139,6 +139,22 @@ def test_slot_endpoint_on_root(self): u'5': {u'title': u'Fifth'}}, u'blocks_layout': {u'items': [5, 1, 3]}}) - # def test_deserializer_slot_not_found(self): - # response = self.api_session.patch('/@slots/left', json={}) - # self.assertEqual(response.status_code, 404) + def test_deserializer_on_slot(self): + response = self.api_session.patch('/@slots/left', json={}) + self.assertEqual(response.status_code, 204) + + def test_deserializer_on_slot_with_data(self): + response = self.api_session.patch('/@slots/left', json={ + 'blocks': { + 1: {'title': 'First'}, + }, + 'blocks_layout': {'items': [5, 1, 3]} + }) + self.assertEqual(response.status_code, 204) + storage = ISlotStorage(self.portal) + self.assertEqual(storage['left'].blocks, { + 1: {'title': 'First'}, + }) + self.assertEqual(storage['left'].blocks_layout, { + 'items': [5, 1, 3] + }) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 4736a15e78..06318286e7 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -10,7 +10,7 @@ from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase from zope.component import provideAdapter -from zope.interface import implements +from zope.interface import implementer from zope.interface import Interface import unittest @@ -49,8 +49,8 @@ def __repr__(self): return "" .format("/".join(reversed(stack))) +@implementer(ISlotStorage) class SlotsStorage(object): - implements(ISlotStorage) def __init__(self, context): self.context = context From ee327f31aa81b3e1574f424982716278d7e36211 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 11 Feb 2021 23:04:32 +0200 Subject: [PATCH 43/99] Fix deserializer + tests --- src/plone/restapi/deserializer/slots.py | 64 +++++++++++-------- src/plone/restapi/services/slots/get.py | 3 + src/plone/restapi/slots/configure.zcml | 8 +-- .../restapi/tests/test_deserializer_slots.py | 2 +- .../restapi/tests/test_services_slots.py | 64 ++++++++++--------- 5 files changed, 78 insertions(+), 63 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 60741c80c5..67bc30627e 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -3,18 +3,19 @@ """ Slots deserializers """ from plone.restapi.deserializer import json_body -from plone.restapi.events import BlocksRemovedEvent +# from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.slots import Slot from plone.restapi.slots.interfaces import ISlot +from plone.restapi.slots.interfaces import ISlots from plone.restapi.slots.interfaces import ISlotStorage from Products.CMFCore.interfaces import IContentish from Products.CMFPlone.interfaces import IPloneSiteRoot from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers -from zope.event import notify +# from zope.event import notify from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest @@ -38,22 +39,23 @@ def __call__(self, data=None): if not data: return - blocks = copy.deepcopy(data['blocks']) + incoming_blocks = copy.deepcopy(data['blocks']) - existing_blocks = self.slot.blocks + engine = ISlots(self.context) + all_blocks_ids = engine.get_blocks(self.slot.__name__)['blocks'].keys() + parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) - removed_blocks_ids = set(existing_blocks.keys()) - set(blocks.keys()) - removed_blocks = {block_id: existing_blocks[block_id] for block_id in - removed_blocks_ids} + # don't keep blocks that are not in incoming data + for k in self.slot.blocks.keys(): + if not ((k in parent_block_ids) or (k in incoming_blocks.keys())): + del self.slot.blocks[k] - notify(BlocksRemovedEvent(dict(context=self.context, blocks=removed_blocks))) - - # we don't want to store blocks that are inherited - for k, v in blocks.items(): + # don't store blocks that are inherited + for k, v in incoming_blocks.items(): if v.get('_v_inherit'): - del blocks[k] + del incoming_blocks[k] - for id, block_value in blocks.items(): + for id, block_value in incoming_blocks.items(): block_type = block_value.get("@type", "") handlers = [] @@ -68,10 +70,16 @@ def __call__(self, data=None): if not getattr(handler, "disabled", False): block_value = handler(block_value) - blocks[id] = block_value + incoming_blocks[id] = block_value + + self.slot.blocks = incoming_blocks - self.slot.blocks = blocks + # don't keep block ids in layout if they're nowhere in the inheritance tree + all_ids = parent_block_ids + list(self.slot.blocks.keys()) + layout = [b for b in data['blocks_layout']['items'] if b in all_ids] + data['blocks_layout']['items'] = layout self.slot.blocks_layout = data['blocks_layout'] + self.slot._p_changed = True @@ -97,27 +105,27 @@ def __call__(self, data=None): data = json_body(self.request) for name, slot in self.storage.items(): - slotdata = data.get(name, None) - if not slotdata: - notify(BlocksRemovedEvent(dict( - context=self.context, - blocks=slot.blocks - ))) + incoming_data = data.get(name, None) + + # remove the existing slot data if the slot doesn't exist in new data + if not incoming_data: + # notify(BlocksRemovedEvent(dict( + # context=self.context, + # blocks=slot.blocks + # ))) self.storage[name].blocks = {} self.storage[name].blocks_layout = {"items": []} - for name, slotdata in data.items(): + for name, incoming_data in data.items(): if name not in self.storage: - slot = Slot() - self.storage[name] = slot - else: - slot = self.storage[name] + # create new slots when found in incoming data + self.storage[name] = Slot() deserializer = getMultiAdapter( - (self.context, slot, self.request), IDeserializeFromJson + (self.context, self.storage[name], self.request), IDeserializeFromJson ) - deserializer(slotdata) + deserializer(incoming_data) @adapter(IPloneSiteRoot, ISlotStorage, IBrowserRequest) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 933651f7a4..68791634f8 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -11,6 +11,9 @@ from zope.publisher.interfaces import IPublishTraverse +# TODO: write expand + + @implementer(IPublishTraverse) class SlotsGet(Service): """Returns the available slots.""" diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index ddbd59799a..5a7f51ac7b 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -51,9 +51,9 @@ name="slots" /> - + + + + diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 80fdc2ef74..0aa7a1f5cf 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -73,7 +73,7 @@ def test_deserialize_put_some(self): self.assertEqual(list(storage.keys()), ['left', 'right']) self.assertEqual(storage['left'].blocks, {2: {'title': 'Second', 's:isVariantOf': 1}, }) - self.assertEqual(storage['left'].blocks_layout, {"items": [3, 2, 5, 4]}) + self.assertEqual(storage['left'].blocks_layout, {"items": [2]}) self.assertEqual(storage['right'].blocks, {6: {'title': 'Sixth'}, }) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 4f4ddc6499..a6af9bb4d7 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -29,6 +29,7 @@ def setUp(self): self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD) self.populateSite() + self.setup_slots() transaction.commit() def tearDown(self): @@ -80,31 +81,30 @@ def populateSite(self): self.doc = self.portal['folder1']['doc11'] + setRoles(self.portal, TEST_USER_ID, ["Member"]) + + def setup_slots(self): rootstore = ISlotStorage(self.portal) - rootstore['left'] = Slot(**({ - 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [5, 1, 3]} - })) - rootstore['right'] = Slot(**({ - 'blocks': { - 6: {'title': 'First'}, - 7: {'title': 'Third'}, - 8: {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [8, 6, 7]} - })) + rootstore[u'left'] = Slot() + rootstore[u'left'].blocks = { + u'1': {'title': 'First'}, + u'3': {'title': 'Third'}, + u'5': {'title': 'Fifth'}, + } + rootstore[u'left'].blocks_layout = {'items': [u'5', u'1', u'3']} + + rootstore[u'right'] = Slot() + rootstore[u'right'].blocks = { + u'6': {'title': 'First'}, + u'7': {'title': 'Third'}, + u'8': {'title': 'Fifth'}, + } + rootstore[u'right'].blocks_layout = {'items': [u'8', u'6', u'7']} storage = ISlotStorage(self.doc) - storage['left'] = Slot( - blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - blocks_layout={'items': [3, 2]}, - ) - - setRoles(self.portal, TEST_USER_ID, ["Member"]) + storage[u'left'] = Slot() + storage[u'left'].blocks = {u'2': {'s:isVariantOf': u'1', 'title': 'Second'}} + storage[u'left'].blocks_layout = {'items': [u'3', u'2']} def test_slots_endpoint(self): response = self.api_session.get("/@slots") @@ -116,12 +116,12 @@ def test_slots_endpoint(self): u'blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [5, 1, 3]}}, + u'blocks_layout': {u'items': [u'5', u'1', u'3']}}, u'right': {u'@id': u'http://localhost:55001/plone/@slots/right', u'blocks': {u'6': {u'title': u'First'}, u'7': {u'title': u'Third'}, u'8': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [8, 6, 7]}}}} + u'blocks_layout': {u'items': [u'8', u'6', u'7']}}}} ) def test_slot_endpoint(self): @@ -137,24 +137,28 @@ def test_slot_endpoint_on_root(self): u'blocks': {u'1': {u'title': u'First'}, u'3': {u'title': u'Third'}, u'5': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [5, 1, 3]}}) + u'blocks_layout': {u'items': [u'5', u'1', u'3']}}) def test_deserializer_on_slot(self): response = self.api_session.patch('/@slots/left', json={}) self.assertEqual(response.status_code, 204) - def test_deserializer_on_slot_with_data(self): + def test_deserializer_on_slot_with_data_and_missing_slots(self): response = self.api_session.patch('/@slots/left', json={ 'blocks': { - 1: {'title': 'First'}, + u'1': {'title': 'First'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [u'5', u'1', u'3']} }) + transaction.commit() self.assertEqual(response.status_code, 204) storage = ISlotStorage(self.portal) self.assertEqual(storage['left'].blocks, { - 1: {'title': 'First'}, + u'1': {'title': 'First'}, }) self.assertEqual(storage['left'].blocks_layout, { - 'items': [5, 1, 3] + 'items': [u'1'] }) + + # self.setup_slots() + # transaction.commit() From fb0e63770e74cc98c340fd0a169e37270e1d7d37 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 11 Feb 2021 23:37:02 +0200 Subject: [PATCH 44/99] Fix tests; don't use an index to keep block sanity, instead resolve it on serialization and deserialization --- src/plone/restapi/deserializer/slots.py | 7 +- src/plone/restapi/slots/__init__.py | 9 +- .../restapi/tests/test_deserializer_slots.py | 120 ++++++++++-------- 3 files changed, 75 insertions(+), 61 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 67bc30627e..08f755ed69 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -50,10 +50,13 @@ def __call__(self, data=None): if not ((k in parent_block_ids) or (k in incoming_blocks.keys())): del self.slot.blocks[k] - # don't store blocks that are inherited + inherited = [] + # don't store blocks that are inherited, keep only those that really exist for k, v in incoming_blocks.items(): if v.get('_v_inherit'): del incoming_blocks[k] + if k in parent_block_ids: + inherited.append(k) for id, block_value in incoming_blocks.items(): block_type = block_value.get("@type", "") @@ -75,7 +78,7 @@ def __call__(self, data=None): self.slot.blocks = incoming_blocks # don't keep block ids in layout if they're nowhere in the inheritance tree - all_ids = parent_block_ids + list(self.slot.blocks.keys()) + all_ids = parent_block_ids + list(self.slot.blocks.keys()) + inherited layout = [b for b in data['blocks_layout']['items'] if b in all_ids] data['blocks_layout']['items'] = layout self.slot.blocks_layout = data['blocks_layout'] diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 44b3a2f682..dd7050f940 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -100,7 +100,7 @@ def get_blocks(self, name): blocks_layout = [] _replaced = set() - _blockmap = {} + _seen_blocks = {} stack = self.get_fills_stack(name) @@ -112,7 +112,7 @@ def get_blocks(self, name): for uid, block in slot.blocks.items(): block = deepcopy(block) - _blockmap[uid] = block + _seen_blocks[uid] = block if not (uid in blocks or uid in _replaced): other = block.get('s:isVariantOf') or block.get('s:sameAs') @@ -132,11 +132,12 @@ def get_blocks(self, name): for k, v in blocks.items(): if v.get('s:sameAs'): v['_v_inherit'] = True - v.update(self._resolve_block(v, _blockmap)) + v.update(self._resolve_block(v, _seen_blocks)) return { 'blocks': blocks, - 'blocks_layout': {'items': blocks_layout} + 'blocks_layout': {'items': [b for b in blocks_layout + if b in _seen_blocks.keys()]} } def _resolve_block(self, block, blocks): diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 0aa7a1f5cf..597da328a3 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -3,6 +3,7 @@ from plone.dexterity.utils import createContentInContainer from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.slots import Slot +from plone.restapi.slots.interfaces import ISlots from plone.restapi.slots.interfaces import ISlotStorage from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from Products.CMFPlone.tests.PloneTestCase import PloneTestCase @@ -55,48 +56,48 @@ def test_deserialize_put_some(self): deserializer({ "left": { - 'blocks_layout': {'items': [3, 2, 5, 4]}, + 'blocks_layout': {'items': [u'3', u'2', u'5', u'4']}, 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, + u'2': {'title': 'Second', 's:isVariantOf': u'1'}, + u'3': {'title': 'Third', '_v_inherit': True}, + u'5': {'title': 'Fifth', '_v_inherit': True}, }, }, "right": { - 'blocks_layout': {'items': [6, 7]}, + 'blocks_layout': {'items': [u'6', u'7']}, 'blocks': { - 6: {'title': 'Sixth'}, + u'6': {'title': 'Sixth'}, } } }) self.assertEqual(list(storage.keys()), ['left', 'right']) self.assertEqual(storage['left'].blocks, - {2: {'title': 'Second', 's:isVariantOf': 1}, }) - self.assertEqual(storage['left'].blocks_layout, {"items": [2]}) + {u'2': {'title': 'Second', 's:isVariantOf': u'1'}, }) + self.assertEqual(storage['left'].blocks_layout, {"items": [u'2']}) self.assertEqual(storage['right'].blocks, - {6: {'title': 'Sixth'}, }) - self.assertEqual(storage['right'].blocks_layout, {"items": [6, 7]}) + {u'6': {'title': 'Sixth'}, }) + self.assertEqual(storage['right'].blocks_layout, {"items": [u'6']}) def test_delete_all_with_dict(self): storage = ISlotStorage(self.doc) storage['left'] = Slot(**({ 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, + u'1': {'title': 'First'}, + u'3': {'title': 'Third'}, + u'5': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [u'5', u'1', u'3']} })) storage['right'] = Slot(**({ 'blocks': { - 6: {'title': 'First'}, - 7: {'title': 'Third'}, - 8: {'title': 'Fifth'}, + u'6': {'title': 'First'}, + u'7': {'title': 'Third'}, + u'8': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [8, 6, 7]} + 'blocks_layout': {'items': [u'8', u'6', u'7']} })) deserializer = getMultiAdapter( @@ -112,19 +113,19 @@ def test_delete_all_with_empty(self): storage['left'] = Slot(**({ 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, + u'1': {'title': 'First'}, + u'3': {'title': 'Third'}, + u'5': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [u'5', u'1', u'3']} })) storage['right'] = Slot(**({ 'blocks': { - 6: {'title': 'First'}, - 7: {'title': 'Third'}, - 8: {'title': 'Fifth'}, + u'6': {'title': 'First'}, + u'7': {'title': 'Third'}, + u'8': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [8, 6, 7]} + 'blocks_layout': {'items': [u'8', u'6', u'7']} })) deserializer = getMultiAdapter( @@ -141,19 +142,19 @@ def test_delete_and_save(self): storage['left'] = Slot(**({ 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, + u'1': {'title': 'First'}, + u'3': {'title': 'Third'}, + u'5': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [u'5', u'1', u'3']} })) storage['right'] = Slot(**({ 'blocks': { - 6: {'title': 'First'}, - 7: {'title': 'Third'}, - 8: {'title': 'Fifth'}, + u'6': {'title': 'First'}, + u'7': {'title': 'Third'}, + u'8': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [8, 6, 7]} + 'blocks_layout': {'items': [u'8', u'6', u'7']} })) deserializer = getMultiAdapter( @@ -161,11 +162,11 @@ def test_delete_and_save(self): deserializer({ "left": { - 'blocks_layout': {'items': [3, 2, 5, 4]}, + 'blocks_layout': {'items': [u'3', u'2', u'5', u'4']}, 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, + u'2': {'title': 'Second', 's:isVariantOf': u'1'}, + u'3': {'title': 'Third', '_v_inherit': True}, + u'5': {'title': 'Fifth', '_v_inherit': True}, }, }, }) @@ -175,40 +176,49 @@ def test_delete_and_save(self): self.assertEqual(right.blocks_layout, {"items": []}) left = storage['left'] - self.assertEqual(left.blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) - self.assertEqual(left.blocks_layout, {'items': [3, 2, 5, 4]}) + self.assertEqual( + left.blocks, {u'2': {'s:isVariantOf': u'1', 'title': 'Second'}}) + self.assertEqual(left.blocks_layout, {'items': [u'2']}) def test_delete_in_parent_affects_child(self): - docstorage = ISlotStorage(self.doc) - - docstorage['left'] = Slot(**({ + rootstorage = ISlotStorage(self.portal) + rootstorage['left'] = Slot(**({ 'blocks': { - 1: {'title': 'First'}, + u'3': {'title': 'Third'}, + u'5': {'title': 'Fifth'}, }, - 'blocks_layout': {'items': [5, 1, 3]} + 'blocks_layout': {'items': [u'5', u'3']} })) - rootstorage = ISlotStorage(self.portal) - rootstorage['left'] = Slot(**({ + docstorage = ISlotStorage(self.doc) + + docstorage['left'] = Slot(**({ 'blocks': { - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, + u'1': {'title': 'First'}, + # u'5': {'_v_inherit': True}, }, - 'blocks_layout': {'items': [5, 3]} + 'blocks_layout': {'items': [u'5', u'1', u'3']} })) - self.portal.portal_catalog.indexObject(self.doc) + # self.portal.portal_catalog.indexObject(self.doc) deserializer = getMultiAdapter( - (self.doc, rootstorage, self.request), IDeserializeFromJson) + (self.portal, rootstorage, self.request), IDeserializeFromJson) deserializer({ "left": { - 'blocks_layout': {'items': [3]}, + 'blocks_layout': {'items': [u'3']}, 'blocks': { - 3: {'title': 'Third', }, + u'3': {'title': 'Third', }, }, }, }) - self.assertEqual(docstorage['left'].blocks_layout['items'], [1, 3]) + self.assertEqual(rootstorage['left'].blocks, + {u'3': {'title': 'Third', }, }) + self.assertEqual(rootstorage['left'].blocks_layout, + {'items': [u'3']}) + + engine = ISlots(self.doc) + self.assertEqual(engine.get_blocks('left')['blocks_layout']['items'], + [u'1', u'3']) From 03c196268f19e115ce4375f645bf93df8704678b Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 11 Feb 2021 23:42:30 +0200 Subject: [PATCH 45/99] Remove block removed events and associated catalog index, they're not needed --- src/plone/restapi/events.py | 20 -------- .../restapi/profiles/default/catalog.xml | 8 --- src/plone/restapi/slots/configure.zcml | 14 ----- src/plone/restapi/slots/handlers.py | 51 ------------------- .../restapi/tests/test_deserializer_slots.py | 2 - .../upgrades/profiles/0007/catalog.xml | 8 --- 6 files changed, 103 deletions(-) delete mode 100644 src/plone/restapi/events.py delete mode 100644 src/plone/restapi/profiles/default/catalog.xml delete mode 100644 src/plone/restapi/slots/handlers.py delete mode 100644 src/plone/restapi/upgrades/profiles/0007/catalog.xml diff --git a/src/plone/restapi/events.py b/src/plone/restapi/events.py deleted file mode 100644 index 5813c6010d..0000000000 --- a/src/plone/restapi/events.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -from plone.restapi.interfaces import IBlockRemovedEvent -from plone.restapi.interfaces import IBlocksRemovedEvent -from zope.interface import implementer -from zope.interface.interfaces import ObjectEvent - - -@implementer(IBlockRemovedEvent) -class BlockRemovedEvent(ObjectEvent): - - def __init__(self, object): - self.object = object - - -@implementer(IBlocksRemovedEvent) -class BlocksRemovedEvent(ObjectEvent): - - def __init__(self, object): - self.object = object diff --git a/src/plone/restapi/profiles/default/catalog.xml b/src/plone/restapi/profiles/default/catalog.xml deleted file mode 100644 index 038792d169..0000000000 --- a/src/plone/restapi/profiles/default/catalog.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index 5a7f51ac7b..dff75fb0fa 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -30,15 +30,6 @@ for="Products.CMFCore.interfaces.IContentish" /> - - - - - - - - diff --git a/src/plone/restapi/slots/handlers.py b/src/plone/restapi/slots/handlers.py deleted file mode 100644 index a9895cc5ed..0000000000 --- a/src/plone/restapi/slots/handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- - -from plone.api import portal -from plone.restapi.slots.interfaces import ISlotStorage - - -def handle_block_removed_event(event): - info = event.object - catalog = portal.get_tool('portal_catalog') - blockid = info['blockid'] - brains = catalog.searchResults(slot_block_ids=blockid) - - for brain in brains: - obj = brain.getObject() - - slots = ISlotStorage(obj) - for slot in slots.values(): - if blockid in slot.blocks_layout['items']: - slot.blocks_layout['items'] = [ - bid for bid in slot.blocks_layout['items'] - if bid != blockid - ] - slot._p_changed = True - - catalog.reindexObject(obj, idxs=['slot_block_ids']) - - -def handle_blocks_removed_event(event): - """ Update the slot blocks layout for objects that reference some blocks - """ - - # TODO: needs rewrite - info = event.object - catalog = portal.get_tool('portal_catalog') - blockids = info['blocks'].keys() - brains = catalog.searchResults(slot_block_ids={'query': blockids, 'operator': 'or'}) - set_block_ids = set(blockids) - - for brain in brains: - obj = brain.getObject() - - slots = ISlotStorage(obj) - for slot in slots.values(): - if set_block_ids.intersection(slot.blocks_layout['items']): - slot.blocks_layout['items'] = [ - bid for bid in slot.blocks_layout['items'] - if bid not in blockids - ] - slot._p_changed = True - - catalog.reindexObject(obj, idxs=['slot_block_ids']) diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 597da328a3..f3afac8b28 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -200,8 +200,6 @@ def test_delete_in_parent_affects_child(self): 'blocks_layout': {'items': [u'5', u'1', u'3']} })) - # self.portal.portal_catalog.indexObject(self.doc) - deserializer = getMultiAdapter( (self.portal, rootstorage, self.request), IDeserializeFromJson) diff --git a/src/plone/restapi/upgrades/profiles/0007/catalog.xml b/src/plone/restapi/upgrades/profiles/0007/catalog.xml deleted file mode 100644 index 038792d169..0000000000 --- a/src/plone/restapi/upgrades/profiles/0007/catalog.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - From cd90fba503a3c702d9b0425124025b1c0eec7c0e Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 11 Feb 2021 23:44:19 +0200 Subject: [PATCH 46/99] Run black --- src/plone/restapi/deserializer/blocks.py | 9 +- src/plone/restapi/deserializer/slots.py | 17 +- src/plone/restapi/interfaces.py | 6 +- src/plone/restapi/serializer/slots.py | 13 +- src/plone/restapi/services/slots/get.py | 12 +- src/plone/restapi/slots/__init__.py | 43 +- src/plone/restapi/slots/indexers.py | 2 +- src/plone/restapi/slots/interfaces.py | 2 +- .../restapi/tests/test_deserializer_slots.py | 305 +++++++++------ .../restapi/tests/test_serializer_slots.py | 272 ++++++++----- .../restapi/tests/test_services_slots.py | 119 +++--- src/plone/restapi/tests/test_slots.py | 368 ++++++++++-------- 12 files changed, 680 insertions(+), 488 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index a24369d0ac..e49a9d9670 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -35,7 +35,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1:] + path = path[len(portal_url) + 1 :] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") @@ -183,10 +183,9 @@ def __call__(self, block): @adapter(IBlocks, IBrowserRequest) @implementer(IBlockFieldDeserializationTransformer) class VolatileSmartField(object): - """ When deserializing block values, delete all block fields that start with `_v_` - """ + """When deserializing block values, delete all block fields that start with `_v_`""" - order = float('inf') + order = float("inf") block_type = None def __init__(self, context, request): @@ -195,7 +194,7 @@ def __init__(self, context, request): def __call__(self, block): for k, v in block.items(): - if k.startswith('_v_'): + if k.startswith("_v_"): del block[k] return block diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 08f755ed69..ca7e2ea3b5 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -3,6 +3,7 @@ """ Slots deserializers """ from plone.restapi.deserializer import json_body + # from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson @@ -15,6 +16,7 @@ from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers + # from zope.event import notify from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest @@ -39,10 +41,10 @@ def __call__(self, data=None): if not data: return - incoming_blocks = copy.deepcopy(data['blocks']) + incoming_blocks = copy.deepcopy(data["blocks"]) engine = ISlots(self.context) - all_blocks_ids = engine.get_blocks(self.slot.__name__)['blocks'].keys() + all_blocks_ids = engine.get_blocks(self.slot.__name__)["blocks"].keys() parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) # don't keep blocks that are not in incoming data @@ -53,7 +55,7 @@ def __call__(self, data=None): inherited = [] # don't store blocks that are inherited, keep only those that really exist for k, v in incoming_blocks.items(): - if v.get('_v_inherit'): + if v.get("_v_inherit"): del incoming_blocks[k] if k in parent_block_ids: inherited.append(k) @@ -79,9 +81,9 @@ def __call__(self, data=None): # don't keep block ids in layout if they're nowhere in the inheritance tree all_ids = parent_block_ids + list(self.slot.blocks.keys()) + inherited - layout = [b for b in data['blocks_layout']['items'] if b in all_ids] - data['blocks_layout']['items'] = layout - self.slot.blocks_layout = data['blocks_layout'] + layout = [b for b in data["blocks_layout"]["items"] if b in all_ids] + data["blocks_layout"]["items"] = layout + self.slot.blocks_layout = data["blocks_layout"] self.slot._p_changed = True @@ -95,8 +97,7 @@ class SlotDeserializerRoot(SlotDeserializer): @adapter(IContentish, ISlotStorage, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotsDeserializer(object): - """ Default deserializer of slots - """ + """Default deserializer of slots""" def __init__(self, context, storage, request): self.context = context diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index f5540d60c7..952be69a64 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -214,10 +214,8 @@ def __call__(value): class IBlocksRemovedEvent(IObjectEvent): - """ A bunch of blocks have been removed - """ + """A bunch of blocks have been removed""" class IBlockRemovedEvent(IObjectEvent): - """ A block has been removed - """ + """A block has been removed""" diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index c13545019c..fb3d4a112f 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -35,7 +35,7 @@ def __call__(self): # a dict with blocks and blocks_layout data = ISlots(self.context).get_blocks(name) - blocks = copy.deepcopy(data['blocks']) + blocks = copy.deepcopy(data["blocks"]) for id, block_value in blocks.items(): block_type = block_value.get("@type", "") @@ -55,7 +55,7 @@ def __call__(self): return { "@id": "{0}/{1}/{2}".format(self.context.absolute_url(), SERVICE_ID, name), "blocks": blocks, - "blocks_layout": data['blocks_layout'] + "blocks_layout": data["blocks_layout"], } @@ -71,10 +71,7 @@ def __init__(self, context, storage, request): def __call__(self): base_url = self.context.absolute_url() - result = { - '@id': '{}/{}'.format(base_url, SERVICE_ID), - "items": {} - } + result = {"@id": "{}/{}".format(base_url, SERVICE_ID), "items": {}} engine = ISlots(self.context) slot_names = engine.discover_slots() @@ -83,7 +80,7 @@ def __call__(self): for name in slot_names: slot = self.storage.get(name, marker) - if slot is marker: # if slot is not on this level, we create a fake one + if slot is marker: # if slot is not on this level, we create a fake one slot = Slot() slot.__parent__ = self.storage slot.__name__ = name @@ -91,6 +88,6 @@ def __call__(self): serializer = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) - result['items'][name] = serializer() + result["items"][name] = serializer() return result diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 68791634f8..59fb315050 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -41,7 +41,7 @@ def reply(self): ) result = adapter() - result['edit_slots'] = json_compatible(self.editable_slots) + result["edit_slots"] = json_compatible(self.editable_slots) # update "edit:True" editable status in slots # for k, v in result['items'].items(): @@ -62,16 +62,14 @@ def replySlot(self): marker = object() storage = ISlotStorage(self.context) slot = storage.get(name, marker) - if slot is marker: # if slot is not on this level, we create a fake one - slot = Slot() # TODO: replace with a DummyProxySlot + if slot is marker: # if slot is not on this level, we create a fake one + slot = Slot() # TODO: replace with a DummyProxySlot slot.__parent__ = self.storage slot.__name__ = name - result = getMultiAdapter( - (self.context, slot, self.request), ISerializeToJson - )() + result = getMultiAdapter((self.context, slot, self.request), ISerializeToJson)() - result['edit'] = name in self.editable_slots + result["edit"] = name in self.editable_slots # TODO: add transaction doom, to deal with annotations created by ISlotStorage ? return result diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index dd7050f940..d90c48de77 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -21,10 +21,7 @@ SLOTS_KEY = "plone.restapi.slots" -DEFAULT_SLOT_DATA = { - "blocks_layout": {"items": []}, - "blocks": {} -} +DEFAULT_SLOT_DATA = {"blocks_layout": {"items": []}, "blocks": {}} @adapter(IContentish) @@ -53,8 +50,7 @@ def __init__(self, **data): @implementer(ISlots) @adapter(ITraversable) class Slots(object): - """ The slots engine provides slots functionality for a content item - """ + """The slots engine provides slots functionality for a content item""" def __init__(self, context): self.context = context @@ -115,33 +111,34 @@ def get_blocks(self, name): _seen_blocks[uid] = block if not (uid in blocks or uid in _replaced): - other = block.get('s:isVariantOf') or block.get('s:sameAs') + other = block.get("s:isVariantOf") or block.get("s:sameAs") if other: _replaced.add(other) blocks[uid] = block if level > 0: - block['_v_inherit'] = True + block["_v_inherit"] = True - for uid in slot.blocks_layout['items']: + for uid in slot.blocks_layout["items"]: if not (uid in blocks_layout or uid in _replaced): blocks_layout.append(uid) level += 1 for k, v in blocks.items(): - if v.get('s:sameAs'): - v['_v_inherit'] = True + if v.get("s:sameAs"): + v["_v_inherit"] = True v.update(self._resolve_block(v, _seen_blocks)) return { - 'blocks': blocks, - 'blocks_layout': {'items': [b for b in blocks_layout - if b in _seen_blocks.keys()]} + "blocks": blocks, + "blocks_layout": { + "items": [b for b in blocks_layout if b in _seen_blocks.keys()] + }, } def _resolve_block(self, block, blocks): - sameAs = block.get('s:sameAs') + sameAs = block.get("s:sameAs") if sameAs: return self._resolve_block(blocks[sameAs], blocks) @@ -151,12 +148,12 @@ def _resolve_block(self, block, blocks): def save_data_to_slot(self, slot, data): to_save = {} - for key in data['blocks_layout']['items']: - block = data['blocks'][key] - if not (block.get('s:sameOf') or block.get('_v_inherit')): + for key in data["blocks_layout"]["items"]: + block = data["blocks"][key] + if not (block.get("s:sameOf") or block.get("_v_inherit")): to_save[key] = block - slot.blocks_layout = data['blocks_layout'] + slot.blocks_layout = data["blocks_layout"] slot.blocks = to_save slot._p_changed = True @@ -174,7 +171,9 @@ def get_editable_slots(self): registry = getUtility(IRegistry) records = registry.forInterface(ISlotSettings) - content_slots = [s for s in - [line.strip() for line in (records.content_slots or [])] - if s in slot_names] + content_slots = [ + s + for s in [line.strip() for line in (records.content_slots or [])] + if s in slot_names + ] return content_slots diff --git a/src/plone/restapi/slots/indexers.py b/src/plone/restapi/slots/indexers.py index 1716f1b502..04aefe8dae 100644 --- a/src/plone/restapi/slots/indexers.py +++ b/src/plone/restapi/slots/indexers.py @@ -15,6 +15,6 @@ def slot_block_ids(obj): blocks = [] storage = ISlotStorage(obj) for name, slot in storage.items(): - blocks.extend(slot.blocks_layout['items']) + blocks.extend(slot.blocks_layout["items"]) return blocks diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index fd98ba4626..e54708a082 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -22,5 +22,5 @@ class ISlotSettings(Interface): content_slots = List( title=u"Content slots", description=u'Editable slots using "Modify portal content" permission', - value_type=TextLine(title=u"Slot name") + value_type=TextLine(title=u"Slot name"), ) diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index f3afac8b28..3424130d66 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -20,11 +20,11 @@ def setUp(self): self.request = self.layer["request"] self.portal.acl_users.userFolderAddUser( - 'simple_member', 'slots_pw', ["Member"], [] + "simple_member", "slots_pw", ["Member"], [] ) self.portal.acl_users.userFolderAddUser( - 'editor_member', 'slots_pw', ["Editor"], [] + "editor_member", "slots_pw", ["Editor"], [] ) self.make_content() @@ -43,7 +43,8 @@ def make_content(self): def test_deserialize_empty(self): storage = ISlotStorage(self.doc) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) + (self.doc, storage, self.request), IDeserializeFromJson + ) deserializer() self.assertEqual(list(storage.keys()), []) @@ -52,171 +53,231 @@ def test_deserialize_put_some(self): storage = ISlotStorage(self.doc) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) - - deserializer({ - "left": { - 'blocks_layout': {'items': [u'3', u'2', u'5', u'4']}, - 'blocks': { - u'2': {'title': 'Second', 's:isVariantOf': u'1'}, - u'3': {'title': 'Third', '_v_inherit': True}, - u'5': {'title': 'Fifth', '_v_inherit': True}, + (self.doc, storage, self.request), IDeserializeFromJson + ) + + deserializer( + { + "left": { + "blocks_layout": {"items": [u"3", u"2", u"5", u"4"]}, + "blocks": { + u"2": {"title": "Second", "s:isVariantOf": u"1"}, + u"3": {"title": "Third", "_v_inherit": True}, + u"5": {"title": "Fifth", "_v_inherit": True}, + }, + }, + "right": { + "blocks_layout": {"items": [u"6", u"7"]}, + "blocks": { + u"6": {"title": "Sixth"}, + }, }, - }, - "right": { - 'blocks_layout': {'items': [u'6', u'7']}, - 'blocks': { - u'6': {'title': 'Sixth'}, - } } - }) + ) - self.assertEqual(list(storage.keys()), ['left', 'right']) - self.assertEqual(storage['left'].blocks, - {u'2': {'title': 'Second', 's:isVariantOf': u'1'}, }) - self.assertEqual(storage['left'].blocks_layout, {"items": [u'2']}) + self.assertEqual(list(storage.keys()), ["left", "right"]) + self.assertEqual( + storage["left"].blocks, + { + u"2": {"title": "Second", "s:isVariantOf": u"1"}, + }, + ) + self.assertEqual(storage["left"].blocks_layout, {"items": [u"2"]}) - self.assertEqual(storage['right'].blocks, - {u'6': {'title': 'Sixth'}, }) - self.assertEqual(storage['right'].blocks_layout, {"items": [u'6']}) + self.assertEqual( + storage["right"].blocks, + { + u"6": {"title": "Sixth"}, + }, + ) + self.assertEqual(storage["right"].blocks_layout, {"items": [u"6"]}) def test_delete_all_with_dict(self): storage = ISlotStorage(self.doc) - storage['left'] = Slot(**({ - 'blocks': { - u'1': {'title': 'First'}, - u'3': {'title': 'Third'}, - u'5': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'5', u'1', u'3']} - })) - storage['right'] = Slot(**({ - 'blocks': { - u'6': {'title': 'First'}, - u'7': {'title': 'Third'}, - u'8': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'8', u'6', u'7']} - })) + storage["left"] = Slot( + **( + { + "blocks": { + u"1": {"title": "First"}, + u"3": {"title": "Third"}, + u"5": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"5", u"1", u"3"]}, + } + ) + ) + storage["right"] = Slot( + **( + { + "blocks": { + u"6": {"title": "First"}, + u"7": {"title": "Third"}, + u"8": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"8", u"6", u"7"]}, + } + ) + ) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) - deserializer({'left': {}, 'right': {}}) + (self.doc, storage, self.request), IDeserializeFromJson + ) + deserializer({"left": {}, "right": {}}) - left = storage['left'] + left = storage["left"] self.assertEqual(left.blocks, {}) self.assertEqual(left.blocks_layout, {"items": []}) def test_delete_all_with_empty(self): storage = ISlotStorage(self.doc) - storage['left'] = Slot(**({ - 'blocks': { - u'1': {'title': 'First'}, - u'3': {'title': 'Third'}, - u'5': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'5', u'1', u'3']} - })) - storage['right'] = Slot(**({ - 'blocks': { - u'6': {'title': 'First'}, - u'7': {'title': 'Third'}, - u'8': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'8', u'6', u'7']} - })) + storage["left"] = Slot( + **( + { + "blocks": { + u"1": {"title": "First"}, + u"3": {"title": "Third"}, + u"5": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"5", u"1", u"3"]}, + } + ) + ) + storage["right"] = Slot( + **( + { + "blocks": { + u"6": {"title": "First"}, + u"7": {"title": "Third"}, + u"8": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"8", u"6", u"7"]}, + } + ) + ) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) + (self.doc, storage, self.request), IDeserializeFromJson + ) deserializer({}) - left = storage['left'] + left = storage["left"] self.assertEqual(left.blocks, {}) self.assertEqual(left.blocks_layout, {"items": []}) def test_delete_and_save(self): storage = ISlotStorage(self.doc) - storage['left'] = Slot(**({ - 'blocks': { - u'1': {'title': 'First'}, - u'3': {'title': 'Third'}, - u'5': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'5', u'1', u'3']} - })) - storage['right'] = Slot(**({ - 'blocks': { - u'6': {'title': 'First'}, - u'7': {'title': 'Third'}, - u'8': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'8', u'6', u'7']} - })) + storage["left"] = Slot( + **( + { + "blocks": { + u"1": {"title": "First"}, + u"3": {"title": "Third"}, + u"5": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"5", u"1", u"3"]}, + } + ) + ) + storage["right"] = Slot( + **( + { + "blocks": { + u"6": {"title": "First"}, + u"7": {"title": "Third"}, + u"8": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"8", u"6", u"7"]}, + } + ) + ) deserializer = getMultiAdapter( - (self.doc, storage, self.request), IDeserializeFromJson) - - deserializer({ - "left": { - 'blocks_layout': {'items': [u'3', u'2', u'5', u'4']}, - 'blocks': { - u'2': {'title': 'Second', 's:isVariantOf': u'1'}, - u'3': {'title': 'Third', '_v_inherit': True}, - u'5': {'title': 'Fifth', '_v_inherit': True}, + (self.doc, storage, self.request), IDeserializeFromJson + ) + + deserializer( + { + "left": { + "blocks_layout": {"items": [u"3", u"2", u"5", u"4"]}, + "blocks": { + u"2": {"title": "Second", "s:isVariantOf": u"1"}, + u"3": {"title": "Third", "_v_inherit": True}, + u"5": {"title": "Fifth", "_v_inherit": True}, + }, }, - }, - }) + } + ) - right = storage['right'] + right = storage["right"] self.assertEqual(right.blocks, {}) self.assertEqual(right.blocks_layout, {"items": []}) - left = storage['left'] + left = storage["left"] self.assertEqual( - left.blocks, {u'2': {'s:isVariantOf': u'1', 'title': 'Second'}}) - self.assertEqual(left.blocks_layout, {'items': [u'2']}) + left.blocks, {u"2": {"s:isVariantOf": u"1", "title": "Second"}} + ) + self.assertEqual(left.blocks_layout, {"items": [u"2"]}) def test_delete_in_parent_affects_child(self): rootstorage = ISlotStorage(self.portal) - rootstorage['left'] = Slot(**({ - 'blocks': { - u'3': {'title': 'Third'}, - u'5': {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [u'5', u'3']} - })) + rootstorage["left"] = Slot( + **( + { + "blocks": { + u"3": {"title": "Third"}, + u"5": {"title": "Fifth"}, + }, + "blocks_layout": {"items": [u"5", u"3"]}, + } + ) + ) docstorage = ISlotStorage(self.doc) - docstorage['left'] = Slot(**({ - 'blocks': { - u'1': {'title': 'First'}, - # u'5': {'_v_inherit': True}, - }, - 'blocks_layout': {'items': [u'5', u'1', u'3']} - })) + docstorage["left"] = Slot( + **( + { + "blocks": { + u"1": {"title": "First"}, + # u'5': {'_v_inherit': True}, + }, + "blocks_layout": {"items": [u"5", u"1", u"3"]}, + } + ) + ) deserializer = getMultiAdapter( - (self.portal, rootstorage, self.request), IDeserializeFromJson) + (self.portal, rootstorage, self.request), IDeserializeFromJson + ) - deserializer({ - "left": { - 'blocks_layout': {'items': [u'3']}, - 'blocks': { - u'3': {'title': 'Third', }, + deserializer( + { + "left": { + "blocks_layout": {"items": [u"3"]}, + "blocks": { + u"3": { + "title": "Third", + }, + }, }, - }, - }) + } + ) - self.assertEqual(rootstorage['left'].blocks, - {u'3': {'title': 'Third', }, }) - self.assertEqual(rootstorage['left'].blocks_layout, - {'items': [u'3']}) + self.assertEqual( + rootstorage["left"].blocks, + { + u"3": { + "title": "Third", + }, + }, + ) + self.assertEqual(rootstorage["left"].blocks_layout, {"items": [u"3"]}) engine = ISlots(self.doc) - self.assertEqual(engine.get_blocks('left')['blocks_layout']['items'], - [u'1', u'3']) + self.assertEqual( + engine.get_blocks("left")["blocks_layout"]["items"], [u"1", u"3"] + ) diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index c6477578be..f582d3885a 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -24,7 +24,8 @@ def setUp(self): def serialize(self, context, slot_or_storage): serializer = getMultiAdapter( - (context, slot_or_storage, self.request), ISerializeToJson) + (context, slot_or_storage, self.request), ISerializeToJson + ) return serializer() def make_content(self): @@ -40,134 +41,197 @@ def make_content(self): def test_slot_empty(self): storage = ISlotStorage(self.portal) - storage['left'] = Slot() - - res = self.serialize(self.portal, storage['left']) - self.assertEqual(res, { - '@id': 'http://nohost/plone/@slots/left', - 'blocks': {}, - 'blocks_layout': {'items': []} - }) + storage["left"] = Slot() + + res = self.serialize(self.portal, storage["left"]) + self.assertEqual( + res, + { + "@id": "http://nohost/plone/@slots/left", + "blocks": {}, + "blocks_layout": {"items": []}, + }, + ) def test_slot(self): storage = ISlotStorage(self.portal) - storage['left'] = Slot(**({ - 'blocks': {1: {}, 2: {}, 3: {}, }, - 'blocks_layout': {'items': [1, 2, 3]} - })) - - res = self.serialize(self.portal, storage['left']) - self.assertEqual(res, { - '@id': 'http://nohost/plone/@slots/left', - 'blocks_layout': {'items': [1, 2, 3]}, - 'blocks': {1: {}, 2: {}, 3: {}} - }) + storage["left"] = Slot( + **( + { + "blocks": { + 1: {}, + 2: {}, + 3: {}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + } + ) + ) + + res = self.serialize(self.portal, storage["left"]) + self.assertEqual( + res, + { + "@id": "http://nohost/plone/@slots/left", + "blocks_layout": {"items": [1, 2, 3]}, + "blocks": {1: {}, 2: {}, 3: {}}, + }, + ) def test_slot_deep(self): rootstore = ISlotStorage(self.portal) - rootstore['left'] = Slot(**({ - 'blocks': {1: {}, 2: {}, 3: {}, }, - 'blocks_layout': {'items': [1, 2, 3]} - })) + rootstore["left"] = Slot( + **( + { + "blocks": { + 1: {}, + 2: {}, + 3: {}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + } + ) + ) storage = ISlotStorage(self.doc) - storage['left'] = Slot() - res = self.serialize(self.doc, storage['left']) - self.assertEqual(res, { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'blocks': {1: {u'_v_inherit': True}, - 2: {u'_v_inherit': True}, - 3: {u'_v_inherit': True}}, - 'blocks_layout': {'items': [1, 2, 3]}}) + storage["left"] = Slot() + res = self.serialize(self.doc, storage["left"]) + self.assertEqual( + res, + { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", + "blocks": { + 1: {u"_v_inherit": True}, + 2: {u"_v_inherit": True}, + 3: {u"_v_inherit": True}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + }, + ) def test_data_override_with_isVariant(self): rootstore = ISlotStorage(self.portal) - rootstore['left'] = Slot(**({ - 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - }, - 'blocks_layout': {'items': [1, 3]} - })) + rootstore["left"] = Slot( + **( + { + "blocks": { + 1: {"title": "First"}, + 3: {"title": "Third"}, + }, + "blocks_layout": {"items": [1, 3]}, + } + ) + ) storage = ISlotStorage(self.doc) - storage['left'] = Slot( - blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - blocks_layout={'items': [2]}, + storage["left"] = Slot( + blocks={2: {"s:isVariantOf": 1, "title": "Second"}}, + blocks_layout={"items": [2]}, + ) + res = self.serialize(self.doc, storage["left"]) + self.assertEqual( + res, + { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", + "blocks": { + 2: {u"s:isVariantOf": 1, u"title": u"Second"}, + 3: {u"_v_inherit": True, u"title": u"Third"}, + }, + "blocks_layout": {"items": [2, 3]}, + }, ) - res = self.serialize(self.doc, storage['left']) - self.assertEqual(res, { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, - 3: {u'_v_inherit': True, u'title': u'Third'}}, - 'blocks_layout': {'items': [2, 3]}}) def test_change_order_from_layout(self): rootstore = ISlotStorage(self.portal) - rootstore['left'] = Slot(**({ - 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [5, 1, 3]} - })) + rootstore["left"] = Slot( + **( + { + "blocks": { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + "blocks_layout": {"items": [5, 1, 3]}, + } + ) + ) storage = ISlotStorage(self.doc) - storage['left'] = Slot( - blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - blocks_layout={'items': [3, 2]}, - ) - res = self.serialize(self.doc, storage['left']) - self.assertEqual(res, { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'blocks': {2: {u's:isVariantOf': 1, u'title': u'Second'}, - 3: {u'_v_inherit': True, u'title': u'Third'}, - 5: {u'_v_inherit': True, u'title': u'Fifth'}}, - 'blocks_layout': {'items': [3, 2, 5]}}) + storage["left"] = Slot( + blocks={2: {"s:isVariantOf": 1, "title": "Second"}}, + blocks_layout={"items": [3, 2]}, + ) + res = self.serialize(self.doc, storage["left"]) + self.assertEqual( + res, + { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", + "blocks": { + 2: {u"s:isVariantOf": 1, u"title": u"Second"}, + 3: {u"_v_inherit": True, u"title": u"Third"}, + 5: {u"_v_inherit": True, u"title": u"Fifth"}, + }, + "blocks_layout": {"items": [3, 2, 5]}, + }, + ) def test_serialize_storage(self): rootstore = ISlotStorage(self.portal) - rootstore['left'] = Slot(**({ - 'blocks': { - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [5, 1, 3]} - })) - rootstore['right'] = Slot(**({ - 'blocks': { - 6: {'title': 'First'}, - 7: {'title': 'Third'}, - 8: {'title': 'Fifth'}, - }, - 'blocks_layout': {'items': [8, 6, 7]} - })) + rootstore["left"] = Slot( + **( + { + "blocks": { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + "blocks_layout": {"items": [5, 1, 3]}, + } + ) + ) + rootstore["right"] = Slot( + **( + { + "blocks": { + 6: {"title": "First"}, + 7: {"title": "Third"}, + 8: {"title": "Fifth"}, + }, + "blocks_layout": {"items": [8, 6, 7]}, + } + ) + ) storage = ISlotStorage(self.doc) - storage['left'] = Slot( - blocks={2: {'s:isVariantOf': 1, 'title': 'Second'}}, - blocks_layout={'items': [3, 2]}, + storage["left"] = Slot( + blocks={2: {"s:isVariantOf": 1, "title": "Second"}}, + blocks_layout={"items": [3, 2]}, ) res = self.serialize(self.doc, storage) - self.assertEqual(res, { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots', - 'items': { - u'left': { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/left', - 'blocks': { - 2: {u's:isVariantOf': 1, u'title': u'Second'}, - 3: {u'_v_inherit': True, u'title': u'Third'}, - 5: {u'_v_inherit': True, u'title': u'Fifth'} + self.assertEqual( + res, + { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots", + "items": { + u"left": { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", + "blocks": { + 2: {u"s:isVariantOf": 1, u"title": u"Second"}, + 3: {u"_v_inherit": True, u"title": u"Third"}, + 5: {u"_v_inherit": True, u"title": u"Fifth"}, + }, + "blocks_layout": {"items": [3, 2, 5]}, + }, + u"right": { + "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/right", + "blocks": { + 6: {u"title": u"First", u"_v_inherit": True}, + 7: {u"title": u"Third", u"_v_inherit": True}, + 8: {u"title": u"Fifth", u"_v_inherit": True}, + }, + "blocks_layout": {"items": [8, 6, 7]}, }, - 'blocks_layout': {'items': [3, 2, 5]}}, - u'right': { - '@id': 'http://nohost/plone/documents/company-a/doc-1/@slots/right', - 'blocks': { - 6: {u'title': u'First', u'_v_inherit': True}, - 7: {u'title': u'Third', u'_v_inherit': True}, - 8: {u'title': u'Fifth', u'_v_inherit': True} - }, - 'blocks_layout': {'items': [8, 6, 7]}}}}) + }, + }, + ) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index a6af9bb4d7..7d9e9c03b8 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -79,49 +79,62 @@ def populateSite(self): folder21.invokeFactory("Document", "doc211") folder21.invokeFactory("Document", "doc212") - self.doc = self.portal['folder1']['doc11'] + self.doc = self.portal["folder1"]["doc11"] setRoles(self.portal, TEST_USER_ID, ["Member"]) def setup_slots(self): rootstore = ISlotStorage(self.portal) - rootstore[u'left'] = Slot() - rootstore[u'left'].blocks = { - u'1': {'title': 'First'}, - u'3': {'title': 'Third'}, - u'5': {'title': 'Fifth'}, + rootstore[u"left"] = Slot() + rootstore[u"left"].blocks = { + u"1": {"title": "First"}, + u"3": {"title": "Third"}, + u"5": {"title": "Fifth"}, } - rootstore[u'left'].blocks_layout = {'items': [u'5', u'1', u'3']} + rootstore[u"left"].blocks_layout = {"items": [u"5", u"1", u"3"]} - rootstore[u'right'] = Slot() - rootstore[u'right'].blocks = { - u'6': {'title': 'First'}, - u'7': {'title': 'Third'}, - u'8': {'title': 'Fifth'}, + rootstore[u"right"] = Slot() + rootstore[u"right"].blocks = { + u"6": {"title": "First"}, + u"7": {"title": "Third"}, + u"8": {"title": "Fifth"}, } - rootstore[u'right'].blocks_layout = {'items': [u'8', u'6', u'7']} + rootstore[u"right"].blocks_layout = {"items": [u"8", u"6", u"7"]} storage = ISlotStorage(self.doc) - storage[u'left'] = Slot() - storage[u'left'].blocks = {u'2': {'s:isVariantOf': u'1', 'title': 'Second'}} - storage[u'left'].blocks_layout = {'items': [u'3', u'2']} + storage[u"left"] = Slot() + storage[u"left"].blocks = {u"2": {"s:isVariantOf": u"1", "title": "Second"}} + storage[u"left"].blocks_layout = {"items": [u"3", u"2"]} def test_slots_endpoint(self): response = self.api_session.get("/@slots") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), { - u'@id': u'http://localhost:55001/plone/@slots', - u'edit_slots': [u'right', u'left'], - u'items': {u'left': {u'@id': u'http://localhost:55001/plone/@slots/left', - u'blocks': {u'1': {u'title': u'First'}, - u'3': {u'title': u'Third'}, - u'5': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [u'5', u'1', u'3']}}, - u'right': {u'@id': u'http://localhost:55001/plone/@slots/right', - u'blocks': {u'6': {u'title': u'First'}, - u'7': {u'title': u'Third'}, - u'8': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [u'8', u'6', u'7']}}}} + self.assertEqual( + response.json(), + { + u"@id": u"http://localhost:55001/plone/@slots", + u"edit_slots": [u"right", u"left"], + u"items": { + u"left": { + u"@id": u"http://localhost:55001/plone/@slots/left", + u"blocks": { + u"1": {u"title": u"First"}, + u"3": {u"title": u"Third"}, + u"5": {u"title": u"Fifth"}, + }, + u"blocks_layout": {u"items": [u"5", u"1", u"3"]}, + }, + u"right": { + u"@id": u"http://localhost:55001/plone/@slots/right", + u"blocks": { + u"6": {u"title": u"First"}, + u"7": {u"title": u"Third"}, + u"8": {u"title": u"Fifth"}, + }, + u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, + }, + }, + }, ) def test_slot_endpoint(self): @@ -131,34 +144,44 @@ def test_slot_endpoint(self): def test_slot_endpoint_on_root(self): response = self.api_session.get("/@slots/left") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), { - u'@id': u'http://localhost:55001/plone/@slots/left', - u'edit': True, - u'blocks': {u'1': {u'title': u'First'}, - u'3': {u'title': u'Third'}, - u'5': {u'title': u'Fifth'}}, - u'blocks_layout': {u'items': [u'5', u'1', u'3']}}) + self.assertEqual( + response.json(), + { + u"@id": u"http://localhost:55001/plone/@slots/left", + u"edit": True, + u"blocks": { + u"1": {u"title": u"First"}, + u"3": {u"title": u"Third"}, + u"5": {u"title": u"Fifth"}, + }, + u"blocks_layout": {u"items": [u"5", u"1", u"3"]}, + }, + ) def test_deserializer_on_slot(self): - response = self.api_session.patch('/@slots/left', json={}) + response = self.api_session.patch("/@slots/left", json={}) self.assertEqual(response.status_code, 204) def test_deserializer_on_slot_with_data_and_missing_slots(self): - response = self.api_session.patch('/@slots/left', json={ - 'blocks': { - u'1': {'title': 'First'}, + response = self.api_session.patch( + "/@slots/left", + json={ + "blocks": { + u"1": {"title": "First"}, + }, + "blocks_layout": {"items": [u"5", u"1", u"3"]}, }, - 'blocks_layout': {'items': [u'5', u'1', u'3']} - }) + ) transaction.commit() self.assertEqual(response.status_code, 204) storage = ISlotStorage(self.portal) - self.assertEqual(storage['left'].blocks, { - u'1': {'title': 'First'}, - }) - self.assertEqual(storage['left'].blocks_layout, { - 'items': [u'1'] - }) + self.assertEqual( + storage["left"].blocks, + { + u"1": {"title": "First"}, + }, + ) + self.assertEqual(storage["left"].blocks_layout, {"items": [u"1"]}) # self.setup_slots() # transaction.commit() diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 06318286e7..a3e3736176 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -39,19 +39,18 @@ def __repr__(self): current = self while True: - if (current.__name__): + if current.__name__: stack.append(current.__name__) if current.__parent__: current = current.__parent__ else: break - return "" .format("/".join(reversed(stack))) + return "".format("/".join(reversed(stack))) @implementer(ISlotStorage) class SlotsStorage(object): - def __init__(self, context): self.context = context @@ -61,15 +60,12 @@ def get(self, name): class DummySlot(object): def __init__(self, data={}): - self.blocks_layout = data and data['blocks_layout'] or {"items": []} - self.blocks = data and data['blocks'] or {} + self.blocks_layout = data and data["blocks_layout"] or {"items": []} + self.blocks = data and data["blocks"] or {} @classmethod def from_data(cls, blocks, layout): - res = { - 'blocks_layout': {"items": layout}, - 'blocks': blocks - } + res = {"blocks_layout": {"items": layout}, "blocks": blocks} return cls(res) @@ -81,160 +77,202 @@ def setUp(self): def make_content(self): root = Content() - root['documents'] = Content() - root['documents']['internal'] = Content() - root['documents']['internal']['company-a'] = Content() - root['documents']['internal']['company-a']['doc-1'] = Content() - root['documents']['internal']['company-a']['doc-2'] = Content() - root['documents']['internal']['company-a']['doc-3'] = Content() + root["documents"] = Content() + root["documents"]["internal"] = Content() + root["documents"]["internal"]["company-a"] = Content() + root["documents"]["internal"]["company-a"]["doc-1"] = Content() + root["documents"]["internal"]["company-a"]["doc-2"] = Content() + root["documents"]["internal"]["company-a"]["doc-3"] = Content() - root['images'] = Content() + root["images"] = Content() return root def test_slot_stack_on_root(self): # simple test with one level stack of slots root = self.make_content() - root.slots['left'] = DummySlot({ - 'blocks': {1: {}, 2: {}, 3: {}, }, - 'blocks_layout': {'items': [1, 2, 3]} - }) - root.slots['right'] = DummySlot() + root.slots["left"] = DummySlot( + { + "blocks": { + 1: {}, + 2: {}, + 3: {}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + } + ) + root.slots["right"] = DummySlot() engine = Slots(root) - self.assertEqual(engine.get_fills_stack('bottom'), [None]) + self.assertEqual(engine.get_fills_stack("bottom"), [None]) - right_stack = engine.get_fills_stack('right') + right_stack = engine.get_fills_stack("right") self.assertEqual(len(right_stack), 1) self.assertEqual(right_stack[0].blocks_layout, {"items": []}) self.assertEqual(right_stack[0].blocks, {}) - left_stack = engine.get_fills_stack('left') + left_stack = engine.get_fills_stack("left") self.assertEqual(len(left_stack), 1) - self.assertEqual(left_stack[0].blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left_stack[0].blocks_layout, {"items": [1, 2, 3]}) self.assertEqual(left_stack[0].blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep(self): # the slot stack is inherited further down root = self.make_content() - root.slots['left'] = DummySlot({ - 'blocks': {1: {}, 2: {}, 3: {}, }, - 'blocks_layout': {'items': [1, 2, 3]} - }) - engine = Slots(root['documents']['internal']['company-a']) + root.slots["left"] = DummySlot( + { + "blocks": { + 1: {}, + 2: {}, + 3: {}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + } + ) + engine = Slots(root["documents"]["internal"]["company-a"]) - self.assertEqual(engine.get_fills_stack('bottom'), [None, None, None, None]) - self.assertEqual(engine.get_fills_stack('right'), [None, None, None, None]) + self.assertEqual(engine.get_fills_stack("bottom"), [None, None, None, None]) + self.assertEqual(engine.get_fills_stack("right"), [None, None, None, None]) - left_stack = engine.get_fills_stack('left') + left_stack = engine.get_fills_stack("left") self.assertEqual(len(left_stack), 4) left = left_stack[3] - self.assertEqual(left.blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(left.blocks_layout, {"items": [1, 2, 3]}) self.assertEqual(left.blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_data_in_root(self): # slots stacks up from deepest to shallow root = self.make_content() - root.slots['left'] = DummySlot({ - 'blocks': {1: {}, 2: {}, 3: {}, }, - 'blocks_layout': {'items': [1, 2, 3]} - }) - obj = root['documents']['internal']['company-a'] + root.slots["left"] = DummySlot( + { + "blocks": { + 1: {}, + 2: {}, + 3: {}, + }, + "blocks_layout": {"items": [1, 2, 3]}, + } + ) + obj = root["documents"]["internal"]["company-a"] - slot = DummySlot.from_data({4: {}, 5: {}, 6: {}}, - [4, 5, 6]) + slot = DummySlot.from_data({4: {}, 5: {}, 6: {}}, [4, 5, 6]) - obj.slots['left'] = slot + obj.slots["left"] = slot engine = Slots(obj) - stack = engine.get_fills_stack('left') + stack = engine.get_fills_stack("left") self.assertEqual(stack[1:3], [None, None]) first = stack[0] - self.assertEqual(first.blocks_layout, {'items': [4, 5, 6]}) + self.assertEqual(first.blocks_layout, {"items": [4, 5, 6]}) self.assertEqual(first.blocks, {4: {}, 5: {}, 6: {}}) last = stack[3] - self.assertEqual(last.blocks_layout, {'items': [1, 2, 3]}) + self.assertEqual(last.blocks_layout, {"items": [1, 2, 3]}) self.assertEqual(last.blocks, {1: {}, 2: {}, 3: {}}) def test_slot_stack_deep_with_stack_collapse(self): # get_blocks collapses the stack and marks inherited slots with _v_inherit root = self.make_content() - obj = root['documents']['internal']['company-a'] + obj = root["documents"]["internal"]["company-a"] - root.slots['left'] = DummySlot.from_data({1: {}, 2: {}, 3: {}, }, [1, 2, 3]) + root.slots["left"] = DummySlot.from_data( + { + 1: {}, + 2: {}, + 3: {}, + }, + [1, 2, 3], + ) - root['documents'].slots['left'] = DummySlot.from_data({4: {}, 5: {}, 6: {}}, - [4, 5, 6]) + root["documents"].slots["left"] = DummySlot.from_data( + {4: {}, 5: {}, 6: {}}, [4, 5, 6] + ) - obj.slots['left'] = DummySlot.from_data({4: {}, 5: {}, 6: {}, 7: {}}, - [4, 5, 6, 7]) + obj.slots["left"] = DummySlot.from_data( + {4: {}, 5: {}, 6: {}, 7: {}}, [4, 5, 6, 7] + ) engine = Slots(obj) - left = engine.get_blocks('left') - self.assertEqual(left, { - 'blocks_layout': {'items': [4, 5, 6, 7, 1, 2, 3]}, - 'blocks': { - 4: {}, - 5: {}, - 6: {}, - 1: {'_v_inherit': True, }, - 2: {'_v_inherit': True, }, - 3: {'_v_inherit': True, }, - 7: {} - } - }) + left = engine.get_blocks("left") + self.assertEqual( + left, + { + "blocks_layout": {"items": [4, 5, 6, 7, 1, 2, 3]}, + "blocks": { + 4: {}, + 5: {}, + 6: {}, + 1: { + "_v_inherit": True, + }, + 2: { + "_v_inherit": True, + }, + 3: { + "_v_inherit": True, + }, + 7: {}, + }, + }, + ) def test_block_data_gets_inherited(self): # blocks that are inherited from parents are marked with _v_inherit root = self.make_content() - obj = root['documents']['internal'] + obj = root["documents"]["internal"] - root['documents'].slots['left'] = DummySlot.from_data( - {1: {'title': 'First'}}, [1]) - obj.slots['left'] = DummySlot.from_data({2: {}}, [2]) + root["documents"].slots["left"] = DummySlot.from_data( + {1: {"title": "First"}}, [1] + ) + obj.slots["left"] = DummySlot.from_data({2: {}}, [2]) engine = Slots(obj) - left = engine.get_blocks('left') + left = engine.get_blocks("left") - self.assertEqual(left, { - 'blocks_layout': {'items': [2, 1]}, - 'blocks': { - 1: {'title': 'First', '_v_inherit': True}, - 2: {} - } - }) + self.assertEqual( + left, + { + "blocks_layout": {"items": [2, 1]}, + "blocks": {1: {"title": "First", "_v_inherit": True}, 2: {}}, + }, + ) def test_block_data_gets_override(self): # a child can override the data for a parent block, in a new block root = self.make_content() - root['documents'].slots['left'] = DummySlot.from_data({ - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - }, [1, 3]) + root["documents"].slots["left"] = DummySlot.from_data( + { + 1: {"title": "First"}, + 3: {"title": "Third"}, + }, + [1, 3], + ) - obj = root['documents']['internal'] - obj.slots['left'] = DummySlot.from_data({ - 2: {'s:isVariantOf': 1, 'title': 'Second'}}, - [2]) + obj = root["documents"]["internal"] + obj.slots["left"] = DummySlot.from_data( + {2: {"s:isVariantOf": 1, "title": "Second"}}, [2] + ) engine = Slots(obj) - left = engine.get_blocks('left') - - self.assertEqual(left, { - 'blocks_layout': {'items': [2, 3]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - } - }) + left = engine.get_blocks("left") + + self.assertEqual( + left, + { + "blocks_layout": {"items": [2, 3]}, + "blocks": { + 2: {"title": "Second", "s:isVariantOf": 1}, + 3: {"title": "Third", "_v_inherit": True}, + }, + }, + ) def test_can_change_order_with_sameOf(self): # a child can change the order inherited from its parents @@ -242,29 +280,34 @@ def test_can_change_order_with_sameOf(self): root = self.make_content() - root['documents'].slots['left'] = DummySlot.from_data({ - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, - }, [5, 1, 3]) + root["documents"].slots["left"] = DummySlot.from_data( + { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + [5, 1, 3], + ) - obj = root['documents']['internal'] - obj.slots['left'] = DummySlot.from_data({ - 2: {'s:isVariantOf': 1, 'title': 'Second'}, - 4: {'s:sameAs': 3} - }, [4, 2]) + obj = root["documents"]["internal"] + obj.slots["left"] = DummySlot.from_data( + {2: {"s:isVariantOf": 1, "title": "Second"}, 4: {"s:sameAs": 3}}, [4, 2] + ) engine = Slots(obj) - left = engine.get_blocks('left') - - self.assertEqual(left, { - 'blocks_layout': {'items': [4, 2, 5]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 4: {'title': 'Third', 's:sameAs': 3, '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, - } - }) + left = engine.get_blocks("left") + + self.assertEqual( + left, + { + "blocks_layout": {"items": [4, 2, 5]}, + "blocks": { + 2: {"title": "Second", "s:isVariantOf": 1}, + 4: {"title": "Third", "s:sameAs": 3, "_v_inherit": True}, + 5: {"title": "Fifth", "_v_inherit": True}, + }, + }, + ) def test_can_change_order_from_layout(self): # a child can change the order inherited from parents by simply repositioning @@ -272,48 +315,57 @@ def test_can_change_order_from_layout(self): root = self.make_content() - root['documents'].slots['left'] = DummySlot.from_data({ - 1: {'title': 'First'}, - 3: {'title': 'Third'}, - 5: {'title': 'Fifth'}, - }, [5, 1, 3]) + root["documents"].slots["left"] = DummySlot.from_data( + { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + [5, 1, 3], + ) - obj = root['documents']['internal'] - obj.slots['left'] = DummySlot.from_data({ - 2: {'s:isVariantOf': 1, 'title': 'Second'}, - }, [3, 2]) + obj = root["documents"]["internal"] + obj.slots["left"] = DummySlot.from_data( + { + 2: {"s:isVariantOf": 1, "title": "Second"}, + }, + [3, 2], + ) engine = Slots(obj) - left = engine.get_blocks('left') - - self.assertEqual(left, { - 'blocks_layout': {'items': [3, 2, 5]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, - } - }) + left = engine.get_blocks("left") + + self.assertEqual( + left, + { + "blocks_layout": {"items": [3, 2, 5]}, + "blocks": { + 2: {"title": "Second", "s:isVariantOf": 1}, + 3: {"title": "Third", "_v_inherit": True}, + 5: {"title": "Fifth", "_v_inherit": True}, + }, + }, + ) def test_save_slots(self): data = { - 'blocks_layout': {'items': [3, 2, 5]}, - 'blocks': { - 2: {'title': 'Second', 's:isVariantOf': 1}, - 3: {'title': 'Third', '_v_inherit': True}, - 5: {'title': 'Fifth', '_v_inherit': True}, + "blocks_layout": {"items": [3, 2, 5]}, + "blocks": { + 2: {"title": "Second", "s:isVariantOf": 1}, + 3: {"title": "Third", "_v_inherit": True}, + 5: {"title": "Fifth", "_v_inherit": True}, }, } root = self.make_content() - obj = root['documents']['internal'] + obj = root["documents"]["internal"] engine = Slots(obj) slot = DummySlot() engine.save_data_to_slot(slot, data) - self.assertEqual(slot.blocks, {2: {'s:isVariantOf': 1, 'title': 'Second'}}) - self.assertEqual(slot.blocks_layout, {'items': [3, 2, 5]}) + self.assertEqual(slot.blocks, {2: {"s:isVariantOf": 1, "title": "Second"}}) + self.assertEqual(slot.blocks_layout, {"items": [3, 2, 5]}) class TestSlotsStorage(unittest.TestCase): @@ -341,21 +393,21 @@ def make_content(self): def test_serialize_slots_storage_portal(self): storage = ISlotStorage(self.portal) - self.assertEqual(storage.__name__, 'plone.restapi.slots') + self.assertEqual(storage.__name__, "plone.restapi.slots") def test_serialize_slots_storage(self): storage = ISlotStorage(self.doc) - self.assertEqual(storage.__name__, 'plone.restapi.slots') + self.assertEqual(storage.__name__, "plone.restapi.slots") self.assertTrue(storage.__parent__ is self.doc) self.assertTrue(storage.__parent__ is self.doc) def test_store_slots_in_storage(self): storage = ISlotStorage(self.doc) - storage['left'] = Slot() + storage["left"] = Slot() - self.assertEqual(storage['left'].__name__, 'left') - self.assertTrue(storage['left'].__parent__ is storage) + self.assertEqual(storage["left"].__name__, "left") + self.assertTrue(storage["left"].__parent__ is storage) class TestSlotsEngineIntegration(PloneTestCase): @@ -368,11 +420,11 @@ def setUp(self): self.request = self.layer["request"] self.portal.acl_users.userFolderAddUser( - 'simple_member', 'slots_pw', ["Member"], [] + "simple_member", "slots_pw", ["Member"], [] ) self.portal.acl_users.userFolderAddUser( - 'editor_member', 'slots_pw', ["Editor"], [] + "editor_member", "slots_pw", ["Editor"], [] ) self.make_content() @@ -395,13 +447,13 @@ def test_editable_slots_as_manager(self): self.assertEqual(empty, []) storage = ISlotStorage(self.doc) - storage['left'] = Slot() + storage["left"] = Slot() left = engine.get_editable_slots() - self.assertEqual(left, ['left']) + self.assertEqual(left, ["left"]) def test_editable_slots_as_member(self): - self.login('simple_member') + self.login("simple_member") engine = ISlots(self.doc) @@ -409,19 +461,19 @@ def test_editable_slots_as_member(self): self.assertEqual(empty, []) storage = ISlotStorage(self.doc) - storage['left'] = Slot() + storage["left"] = Slot() left = engine.get_editable_slots() self.assertEqual(left, []) - registry = portal.get_tool('portal_registry') + registry = portal.get_tool("portal_registry") proxy = registry.forInterface(ISlotSettings) - proxy.content_slots = [u'left'] + proxy.content_slots = [u"left"] self.assertEqual(engine.get_editable_slots(), []) def test_editable_slots_as_editor(self): - self.login('editor_member') + self.login("editor_member") engine = ISlots(self.doc) @@ -429,13 +481,13 @@ def test_editable_slots_as_editor(self): self.assertEqual(empty, []) storage = ISlotStorage(self.doc) - storage['left'] = Slot() + storage["left"] = Slot() left = engine.get_editable_slots() self.assertEqual(left, []) - registry = portal.get_tool('portal_registry') + registry = portal.get_tool("portal_registry") proxy = registry.forInterface(ISlotSettings) - proxy.content_slots = [u'left'] + proxy.content_slots = [u"left"] - self.assertEqual(engine.get_editable_slots(), [u'left']) + self.assertEqual(engine.get_editable_slots(), [u"left"]) From a28ea4ed80b4c2df5a34337853c9a9466ce7beb0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 12 Feb 2021 19:26:36 +0200 Subject: [PATCH 47/99] Fix slot --- src/plone/restapi/services/slots/get.py | 2 +- src/plone/restapi/tests/test_services_slots.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 59fb315050..333acfb32a 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -41,7 +41,7 @@ def reply(self): ) result = adapter() - result["edit_slots"] = json_compatible(self.editable_slots) + result["edit_slots"] = json_compatible(sorted(self.editable_slots)) # update "edit:True" editable status in slots # for k, v in result['items'].items(): diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 7d9e9c03b8..123416675c 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -113,7 +113,7 @@ def test_slots_endpoint(self): response.json(), { u"@id": u"http://localhost:55001/plone/@slots", - u"edit_slots": [u"right", u"left"], + u"edit_slots": [u"left", u"right"], u"items": { u"left": { u"@id": u"http://localhost:55001/plone/@slots/left", @@ -182,6 +182,3 @@ def test_deserializer_on_slot_with_data_and_missing_slots(self): }, ) self.assertEqual(storage["left"].blocks_layout, {"items": [u"1"]}) - - # self.setup_slots() - # transaction.commit() From 790d571e640e2911c1a8d10d4f8deb667a7d3a64 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 12 Feb 2021 19:33:47 +0200 Subject: [PATCH 48/99] Simplify test content creation --- .../restapi/tests/test_services_slots.py | 37 ------------------- 1 file changed, 37 deletions(-) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 123416675c..1a9bc1a372 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -36,48 +36,11 @@ def tearDown(self): self.api_session.close() def populateSite(self): - """ - Portal - +-doc1 - +-doc2 - +-doc3 - +-folder1 - +-doc11 - +-doc12 - +-doc13 - +-link1 - +-folder2 - +-doc21 - +-doc22 - +-doc23 - +-file21 - +-folder21 - +-doc211 - +-doc212 - """ setRoles(self.portal, TEST_USER_ID, ["Manager"]) - self.portal.invokeFactory("Document", "doc1") - self.portal.invokeFactory("Document", "doc2") - self.portal.invokeFactory("Document", "doc3") self.portal.invokeFactory("Folder", "folder1") - self.portal.invokeFactory("Link", "link1") - self.portal.link1.remoteUrl = "http://plone.org" - self.portal.link1.reindexObject() folder1 = getattr(self.portal, "folder1") folder1.invokeFactory("Document", "doc11") - folder1.invokeFactory("Document", "doc12") - folder1.invokeFactory("Document", "doc13") - self.portal.invokeFactory("Folder", "folder2") - folder2 = getattr(self.portal, "folder2") - folder2.invokeFactory("Document", "doc21") - folder2.invokeFactory("Document", "doc22") - folder2.invokeFactory("Document", "doc23") - folder2.invokeFactory("File", "file21") - folder2.invokeFactory("Folder", "folder21") - folder21 = getattr(folder2, "folder21") - folder21.invokeFactory("Document", "doc211") - folder21.invokeFactory("Document", "doc212") self.doc = self.portal["folder1"]["doc11"] From bea4d76d24b38f5c6abe8c09c7563307b9d4b462 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 14 Feb 2021 13:00:52 +0200 Subject: [PATCH 49/99] Add skip csrf protection --- src/plone/restapi/services/slots/get.py | 7 +++++++ src/plone/restapi/services/slots/update.py | 5 +++++ src/plone/restapi/slots/__init__.py | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 333acfb32a..eb8725f00e 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -7,9 +7,12 @@ from plone.restapi.slots.interfaces import ISlots from plone.restapi.slots.interfaces import ISlotStorage from zope.component import getMultiAdapter +from zope.interface import alsoProvides from zope.interface import implementer from zope.publisher.interfaces import IPublishTraverse +import plone.protect + # TODO: write expand @@ -27,6 +30,10 @@ def publishTraverse(self, request, name): return self def reply(self): + + if "IDisableCSRFProtection" in dir(plone.protect.interfaces): + alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) + self.engine = ISlots(self.context) self.slot_names = self.engine.discover_slots() self.editable_slots = self.engine.get_editable_slots() diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index 26b5451494..1e3afc6fce 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -11,10 +11,13 @@ from plone.restapi.slots.interfaces import ISlotStorage from zope.component import getMultiAdapter from zope.event import notify +from zope.interface import alsoProvides from zope.interface import implementer from zope.lifecycleevent import ObjectModifiedEvent from zope.publisher.interfaces import IPublishTraverse +import plone.protect + @implementer(IPublishTraverse) class SlotsPatch(Service): @@ -29,6 +32,8 @@ def publishTraverse(self, request, name): return self def reply(self): + if "IDisableCSRFProtection" in dir(plone.protect.interfaces): + alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) if is_locked(self.context, self.request): self.request.response.setStatus(403) return dict(error=dict(type="Forbidden", message="Resource is locked.")) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index d90c48de77..70caff0e47 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -34,7 +34,7 @@ class PersistentSlots(BTreeContainer): @implementer(ISlot) -class Slot(Contained, Persistent): +class Slot(Persistent, Contained): """A container for data pertaining to a single slot""" def __init__(self, **data): From 1922a9d4d3b4766c924cca90530847b376e726e6 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 14 Feb 2021 21:19:50 +0200 Subject: [PATCH 50/99] Use edit status in serializer --- src/plone/restapi/services/slots/get.py | 6 +++--- src/plone/restapi/tests/test_services_slots.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index eb8725f00e..f88f89a985 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -48,11 +48,11 @@ def reply(self): ) result = adapter() - result["edit_slots"] = json_compatible(sorted(self.editable_slots)) + # result["edit_slots"] = json_compatible(sorted(self.editable_slots)) # update "edit:True" editable status in slots - # for k, v in result['items'].items(): - # result['items'][k]['edit'] = k in self.editable_slots + for k, v in result["items"].items(): + result["items"][k]["edit"] = k in self.editable_slots return result diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 1a9bc1a372..7c66d54a87 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -76,7 +76,6 @@ def test_slots_endpoint(self): response.json(), { u"@id": u"http://localhost:55001/plone/@slots", - u"edit_slots": [u"left", u"right"], u"items": { u"left": { u"@id": u"http://localhost:55001/plone/@slots/left", @@ -86,6 +85,7 @@ def test_slots_endpoint(self): u"5": {u"title": u"Fifth"}, }, u"blocks_layout": {u"items": [u"5", u"1", u"3"]}, + u"edit": True, }, u"right": { u"@id": u"http://localhost:55001/plone/@slots/right", @@ -95,6 +95,7 @@ def test_slots_endpoint(self): u"8": {u"title": u"Fifth"}, }, u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, + u"edit": True, }, }, }, From dd20be72cd239732277524e7e6c3414bac2165e9 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 14 Feb 2021 21:20:39 +0200 Subject: [PATCH 51/99] Use edit status in serializer --- src/plone/restapi/services/slots/get.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index f88f89a985..c01350aca9 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from plone.restapi.interfaces import ISerializeToJson -from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service from plone.restapi.slots import Slot from plone.restapi.slots.interfaces import ISlots @@ -48,6 +47,7 @@ def reply(self): ) result = adapter() + # from plone.restapi.serializer.converters import json_compatible # result["edit_slots"] = json_compatible(sorted(self.editable_slots)) # update "edit:True" editable status in slots From 063f4d30f63c6c7155c6c698027852a17b26c279 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 15 Feb 2021 22:45:10 +0200 Subject: [PATCH 52/99] Fix deserializer, don't trigger ObjectModifiedEvent, as it needs its own events --- src/plone/restapi/services/slots/update.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/plone/restapi/services/slots/update.py b/src/plone/restapi/services/slots/update.py index 1e3afc6fce..687b551409 100644 --- a/src/plone/restapi/services/slots/update.py +++ b/src/plone/restapi/services/slots/update.py @@ -8,17 +8,20 @@ from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from plone.restapi.services.locking.locking import is_locked +from plone.restapi.slots import Slot from plone.restapi.slots.interfaces import ISlotStorage from zope.component import getMultiAdapter -from zope.event import notify from zope.interface import alsoProvides from zope.interface import implementer -from zope.lifecycleevent import ObjectModifiedEvent from zope.publisher.interfaces import IPublishTraverse import plone.protect +# from zope.event import notify +# from zope.lifecycleevent import ObjectModifiedEvent + + @implementer(IPublishTraverse) class SlotsPatch(Service): """Update one or all the slots""" @@ -53,7 +56,7 @@ def reply(self): self.request.response.setStatus(400) return dict(error=dict(type="DeserializationError", message=str(e))) - notify(ObjectModifiedEvent(self.context)) + # notify(ObjectModifiedEvent(self.context)) prefer = self.request.getHeader("Prefer") if prefer == "return=representation": @@ -71,6 +74,8 @@ def reply(self): def replySlot(self): name = self.params[0] storage = ISlotStorage(self.context) + if name not in storage: + storage[name] = Slot() slot = storage[name] deserializer = getMultiAdapter( @@ -82,7 +87,7 @@ def replySlot(self): self.request.response.setStatus(400) return dict(error=dict(type="DeserializationError", message=str(e))) - notify(ObjectModifiedEvent(self.context)) + # notify(ObjectModifiedEvent(self.context)) prefer = self.request.getHeader("Prefer") if prefer == "return=representation": From 24be48d292aa382700437e28f2ae2f4729fb0250 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 16 Feb 2021 15:49:01 +0200 Subject: [PATCH 53/99] Fix block transformer for slots --- src/plone/restapi/deserializer/slots.py | 6 +++--- src/plone/restapi/serializer/slots.py | 4 +++- src/plone/restapi/slots/__init__.py | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index ca7e2ea3b5..d6a56e3660 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -3,7 +3,6 @@ """ Slots deserializers """ from plone.restapi.deserializer import json_body - # from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson @@ -16,7 +15,6 @@ from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers - # from zope.event import notify from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest @@ -60,12 +58,14 @@ def __call__(self, data=None): if k in parent_block_ids: inherited.append(k) + slot = self.slot.__of__(self.context) + for id, block_value in incoming_blocks.items(): block_type = block_value.get("@type", "") handlers = [] for h in subscribers( - (self.slot, self.request), + (slot, self.request), IBlockFieldDeserializationTransformer, ): if h.block_type == block_type or h.block_type is None: diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index fb3d4a112f..113dea8b5b 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -37,11 +37,13 @@ def __call__(self): blocks = copy.deepcopy(data["blocks"]) + slot = self.slot.__of__(self.context) + for id, block_value in blocks.items(): block_type = block_value.get("@type", "") handlers = [] for h in subscribers( - (self.context, self.request), IBlockFieldSerializationTransformer + (slot, self.request), IBlockFieldSerializationTransformer ): if h.block_type == block_type or h.block_type is None: handlers.append(h) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 70caff0e47..488037b6cd 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -4,6 +4,7 @@ from .interfaces import ISlots from .interfaces import ISlotStorage from AccessControl.SecurityManagement import getSecurityManager +from Acquisition import Implicit from copy import deepcopy from persistent import Persistent from plone.registry.interfaces import IRegistry @@ -34,7 +35,7 @@ class PersistentSlots(BTreeContainer): @implementer(ISlot) -class Slot(Persistent, Contained): +class Slot(Persistent, Contained, Implicit): """A container for data pertaining to a single slot""" def __init__(self, **data): @@ -118,6 +119,7 @@ def get_blocks(self, name): blocks[uid] = block if level > 0: block["_v_inherit"] = True + block["readOnly"] = True for uid in slot.blocks_layout["items"]: if not (uid in blocks_layout or uid in _replaced): @@ -128,6 +130,7 @@ def get_blocks(self, name): for k, v in blocks.items(): if v.get("s:sameAs"): v["_v_inherit"] = True + block["readOnly"] = True v.update(self._resolve_block(v, _seen_blocks)) return { From d420b874bd63293545c71f4e5de05d1aacff6f06 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 19 Mar 2021 18:15:23 +0200 Subject: [PATCH 54/99] Run black --- src/plone/restapi/deserializer/slots.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index d6a56e3660..52c1813e5f 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -3,6 +3,7 @@ """ Slots deserializers """ from plone.restapi.deserializer import json_body + # from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson @@ -15,6 +16,7 @@ from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers + # from zope.event import notify from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest From c3ae3db7fdd79aabdc941f801c740b547f53c326 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 29 Mar 2021 13:52:00 +0300 Subject: [PATCH 55/99] Add can_manage_slots info to response --- src/plone/restapi/services/slots/get.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index c01350aca9..ade06a1ff4 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +from AccessControl.SecurityManagement import getSecurityManager from plone.restapi.interfaces import ISerializeToJson +from plone.restapi.permissions import ModifySlotsPermission from plone.restapi.services import Service from plone.restapi.slots import Slot from plone.restapi.slots.interfaces import ISlots @@ -54,6 +56,11 @@ def reply(self): for k, v in result["items"].items(): result["items"][k]["edit"] = k in self.editable_slots + sm = getSecurityManager() + + if sm.checkPermission(ModifySlotsPermission, self.context): + result["can_manage_slots"] = True + return result def replySlot(self): From 923f7aded36fe30696adb8ed27fbc7eb3220c0eb Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 29 Mar 2021 14:53:37 +0300 Subject: [PATCH 56/99] Fix a bug in deleting slot fills --- src/plone/restapi/deserializer/slots.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 52c1813e5f..0b32911627 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -3,8 +3,6 @@ """ Slots deserializers """ from plone.restapi.deserializer import json_body - -# from plone.restapi.events import BlocksRemovedEvent from plone.restapi.interfaces import IBlockFieldDeserializationTransformer from plone.restapi.interfaces import IDeserializeFromJson from plone.restapi.slots import Slot @@ -16,14 +14,16 @@ from zope.component import adapter from zope.component import getMultiAdapter from zope.component import subscribers - -# from zope.event import notify from zope.interface import implementer from zope.publisher.interfaces.browser import IBrowserRequest import copy +# from plone.restapi.events import BlocksRemovedEvent +# from zope.event import notify + + @adapter(IContentish, ISlot, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotDeserializer(object): @@ -48,7 +48,7 @@ def __call__(self, data=None): parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) # don't keep blocks that are not in incoming data - for k in self.slot.blocks.keys(): + for k in list(self.slot.blocks.keys()): if not ((k in parent_block_ids) or (k in incoming_blocks.keys())): del self.slot.blocks[k] From 495380edf1249bcdf409875b494d41fd6150bfed Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 30 Mar 2021 21:35:57 +0300 Subject: [PATCH 57/99] Fix another delete --- src/plone/restapi/deserializer/slots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 0b32911627..06111bdf68 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -54,7 +54,7 @@ def __call__(self, data=None): inherited = [] # don't store blocks that are inherited, keep only those that really exist - for k, v in incoming_blocks.items(): + for k, v in list(incoming_blocks.items()): if v.get("_v_inherit"): del incoming_blocks[k] if k in parent_block_ids: From 2ee8de7f5e17a6aadd8fa0cb418357e1eecdb1ac Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 16:08:22 +0300 Subject: [PATCH 58/99] Add transaction doom --- src/plone/restapi/services/slots/get.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index ade06a1ff4..fa3b20431c 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -13,6 +13,7 @@ from zope.publisher.interfaces import IPublishTraverse import plone.protect +import transaction # TODO: write expand @@ -76,6 +77,7 @@ def replySlot(self): marker = object() storage = ISlotStorage(self.context) slot = storage.get(name, marker) + if slot is marker: # if slot is not on this level, we create a fake one slot = Slot() # TODO: replace with a DummyProxySlot slot.__parent__ = self.storage @@ -85,5 +87,5 @@ def replySlot(self): result["edit"] = name in self.editable_slots - # TODO: add transaction doom, to deal with annotations created by ISlotStorage ? + transaction.doom() # avoid writing on read return result From fc8d0004f0f47a556a9f17d257c23e9758de95a0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 17:25:28 +0300 Subject: [PATCH 59/99] Add full parameter --- src/plone/restapi/serializer/slots.py | 9 +++++---- src/plone/restapi/services/slots/get.py | 25 +++++++++++++++++-------- src/plone/restapi/slots/__init__.py | 2 +- src/plone/restapi/slots/interfaces.py | 3 +++ 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 113dea8b5b..70369e04f9 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -29,11 +29,11 @@ def __init__(self, context, slot, request): self.request = request self.slot = slot - def __call__(self): + def __call__(self, full=False): name = self.slot.__name__ # a dict with blocks and blocks_layout - data = ISlots(self.context).get_blocks(name) + data = ISlots(self.context).get_blocks(name, full) blocks = copy.deepcopy(data["blocks"]) @@ -71,7 +71,7 @@ def __init__(self, context, storage, request): self.request = request self.storage = storage - def __call__(self): + def __call__(self, full=False): base_url = self.context.absolute_url() result = {"@id": "{}/{}".format(base_url, SERVICE_ID), "items": {}} @@ -83,6 +83,7 @@ def __call__(self): slot = self.storage.get(name, marker) if slot is marker: # if slot is not on this level, we create a fake one + # TODO: deal with this better, no need for transaction.doom() slot = Slot() slot.__parent__ = self.storage slot.__name__ = name @@ -90,6 +91,6 @@ def __call__(self): serializer = getMultiAdapter( (self.context, slot, self.request), ISerializeToJson ) - result["items"][name] = serializer() + result["items"][name] = serializer(full) return result diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index fa3b20431c..ba370b3d13 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -17,6 +17,16 @@ # TODO: write expand +def is_true(val): + if isinstance(val, bool): + return val + + if val in ['true', 'True', '1', 1]: + return True + elif val in ['false', 'False', '0', 0]: + return False + + return False @implementer(IPublishTraverse) @@ -43,25 +53,23 @@ def reply(self): if self.params and len(self.params) > 0: return self.replySlot() + sm = getSecurityManager() storage = ISlotStorage(self.context) adapter = getMultiAdapter( (self.context, storage, self.request), ISerializeToJson ) - result = adapter() + result = adapter(self.request.form.get('full', False)) # from plone.restapi.serializer.converters import json_compatible # result["edit_slots"] = json_compatible(sorted(self.editable_slots)) - # update "edit:True" editable status in slots - for k, v in result["items"].items(): - result["items"][k]["edit"] = k in self.editable_slots - - sm = getSecurityManager() - if sm.checkPermission(ModifySlotsPermission, self.context): result["can_manage_slots"] = True + for k, v in result["items"].items(): + result["items"][k]["edit"] = k in self.editable_slots + return result def replySlot(self): @@ -83,7 +91,8 @@ def replySlot(self): slot.__parent__ = self.storage slot.__name__ = name - result = getMultiAdapter((self.context, slot, self.request), ISerializeToJson)() + result = getMultiAdapter((self.context, slot, self.request), + ISerializeToJson)(self.request.form.get('full', False)) result["edit"] = name in self.editable_slots diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 488037b6cd..28a9cc0142 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -92,7 +92,7 @@ def get_fills_stack(self, name): return slot_stack - def get_blocks(self, name): + def get_blocks(self, name, block_parent): blocks = {} blocks_layout = [] diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index e54708a082..f3441815fa 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -2,6 +2,7 @@ from plone.restapi.behaviors import IBlocks from zope.interface import Interface +from zope.schema import Bool from zope.schema import List from zope.schema import TextLine @@ -17,6 +18,8 @@ class ISlotStorage(Interface): class ISlot(IBlocks): """Slots follow the IBlocks model""" + block_parent = Bool(title=u"Block inheritance of slot fills") + class ISlotSettings(Interface): content_slots = List( From 28cab5d62e05f6dbfdb1543ee2c25c8b1fbfcf1f Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 17:31:51 +0300 Subject: [PATCH 60/99] Fix deserializer --- src/plone/restapi/deserializer/slots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 06111bdf68..3341ebfe06 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -44,7 +44,7 @@ def __call__(self, data=None): incoming_blocks = copy.deepcopy(data["blocks"]) engine = ISlots(self.context) - all_blocks_ids = engine.get_blocks(self.slot.__name__)["blocks"].keys() + all_blocks_ids = engine.get_blocks(self.slot.__name__, full=True)["blocks"].keys() parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) # don't keep blocks that are not in incoming data From e00ed376f0596fea40a06dbf07db90b704d02720 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 17:35:29 +0300 Subject: [PATCH 61/99] Add block parent --- src/plone/restapi/deserializer/slots.py | 1 + src/plone/restapi/slots/__init__.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 3341ebfe06..6abcf53bf1 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -87,6 +87,7 @@ def __call__(self, data=None): data["blocks_layout"]["items"] = layout self.slot.blocks_layout = data["blocks_layout"] + self.slot.block_parent = data.get('block_parent', False) self.slot._p_changed = True diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 28a9cc0142..cd790cba62 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -127,6 +127,9 @@ def get_blocks(self, name, block_parent): level += 1 + if slot.block_parent: + break + for k, v in blocks.items(): if v.get("s:sameAs"): v["_v_inherit"] = True From 401a7ea930ac5cceb7e27d5ddced746edad5cde1 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 17:42:43 +0300 Subject: [PATCH 62/99] Fix parameters --- src/plone/restapi/slots/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index cd790cba62..f60b7c49e4 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -92,7 +92,7 @@ def get_fills_stack(self, name): return slot_stack - def get_blocks(self, name, block_parent): + def get_blocks(self, name, full=True): blocks = {} blocks_layout = [] @@ -127,7 +127,7 @@ def get_blocks(self, name, block_parent): level += 1 - if slot.block_parent: + if getattr(slot, 'block_parent', False): break for k, v in blocks.items(): From d6583c0314dec3793486ed8435636147bde24fda Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 18:56:42 +0300 Subject: [PATCH 63/99] Improve block_parent implementation --- src/plone/restapi/deserializer/slots.py | 2 +- src/plone/restapi/serializer/slots.py | 28 +++++++++++++++++++++---- src/plone/restapi/services/slots/get.py | 3 ++- src/plone/restapi/slots/__init__.py | 2 +- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 6abcf53bf1..f4c4e6337b 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -44,7 +44,7 @@ def __call__(self, data=None): incoming_blocks = copy.deepcopy(data["blocks"]) engine = ISlots(self.context) - all_blocks_ids = engine.get_blocks(self.slot.__name__, full=True)["blocks"].keys() + all_blocks_ids = engine.get_data(self.slot.__name__, full=True)["blocks"].keys() parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) # don't keep blocks that are not in incoming data diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 70369e04f9..c3b64d6f64 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- + from plone.restapi.interfaces import IBlockFieldSerializationTransformer from plone.restapi.interfaces import ISerializeToJson from plone.restapi.serializer.converters import json_compatible @@ -12,12 +13,27 @@ from zope.interface import implementer from zope.interface import Interface from zope.publisher.interfaces.browser import IBrowserRequest +from zope.schema import getFields import copy SERVICE_ID = "@slots" +_MISSING = object() + + +def serialize_data(slot, request, schema): + result = {} + for name, field in getFields(schema).items(): + if name in ['blocks', 'blocks_layout']: + continue + value = getattr(slot, name, _MISSING) + if value is not _MISSING: + result[json_compatible(name)] = value # assumes JSON-compatible values + + return result + @adapter(Interface, ISlot, IBrowserRequest) @implementer(ISerializeToJson) @@ -33,7 +49,7 @@ def __call__(self, full=False): name = self.slot.__name__ # a dict with blocks and blocks_layout - data = ISlots(self.context).get_blocks(name, full) + data = ISlots(self.context).get_data(name, full) blocks = copy.deepcopy(data["blocks"]) @@ -54,15 +70,19 @@ def __call__(self, full=False): blocks[id] = json_compatible(block_value) - return { + result = { "@id": "{0}/{1}/{2}".format(self.context.absolute_url(), SERVICE_ID, name), "blocks": blocks, "blocks_layout": data["blocks_layout"], } + result.update(**serialize_data(slot, self.request, schema=ISlot)) -@adapter(Interface, ISlotStorage, IBrowserRequest) -@implementer(ISerializeToJson) + return result + + +@ adapter(Interface, ISlotStorage, IBrowserRequest) +@ implementer(ISerializeToJson) class SlotsSerializer(object): """Default slots storage serializer""" diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index ba370b3d13..0e42a4d2a7 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -59,7 +59,8 @@ def reply(self): adapter = getMultiAdapter( (self.context, storage, self.request), ISerializeToJson ) - result = adapter(self.request.form.get('full', False)) + is_full = self.request.form.get('full', False) + result = adapter(is_full) # from plone.restapi.serializer.converters import json_compatible # result["edit_slots"] = json_compatible(sorted(self.editable_slots)) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index f60b7c49e4..edfa251c5a 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -92,7 +92,7 @@ def get_fills_stack(self, name): return slot_stack - def get_blocks(self, name, full=True): + def get_data(self, name, full=True): blocks = {} blocks_layout = [] From e6bd47bb3c8210e0e5f7c8898169c3f1c1b9e02a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Fri, 2 Jul 2021 19:22:55 +0300 Subject: [PATCH 64/99] Pass blocks when full is true --- src/plone/restapi/slots/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index edfa251c5a..a2160abcab 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -127,7 +127,7 @@ def get_data(self, name, full=True): level += 1 - if getattr(slot, 'block_parent', False): + if getattr(slot, 'block_parent', False) and not full: break for k, v in blocks.items(): From cfae76cb8247d066a6c85ee941c195cdd9b560fa Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 5 Jul 2021 16:15:45 +0300 Subject: [PATCH 65/99] Add info about attributes --- src/plone/restapi/slots/interfaces.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index f3441815fa..fadd6050d4 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -27,3 +27,18 @@ class ISlotSettings(Interface): description=u'Editable slots using "Modify portal content" permission', value_type=TextLine(title=u"Slot name"), ) + + +""" +# special slot fill attributes: + +- `s:sameAs`: Ideally blocks uids should be unique across the whole CMS. That's why we + use uuid, right? So, when customizing the order of an inherited slot, a "placeholder" + should be created in the current level. This placeholder would have `s:sameAs` point to + the "overriden" parent. +- `s:isVariantOf`: An inherited block could be "copied" to another level and mutated + there. `s:isVariantOf` points to the original source block. +- `v:hidden`: An inherited block is hidden at this level. To be used with `s:sameAs` to + point to the "parent" block. When publishing slots (in the `@slots` endpoint) the + hidden blocks are included only when run with `full=true`. +""" From f2cdd56aed0f2fc63a6ea1a8aa52db379c774d27 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 6 Jul 2021 06:27:29 +0300 Subject: [PATCH 66/99] Fix _v_ handling; send a _v_original to the frontend --- src/plone/restapi/deserializer/blocks.py | 5 +++-- src/plone/restapi/slots/__init__.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index b126167a58..f4b132f0c1 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -49,7 +49,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1 :] + path = path[len(portal_url) + 1:] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") @@ -241,7 +241,8 @@ def __init__(self, context, request): def __call__(self, block): for k, v in block.items(): if k.startswith("_v_"): - del block[k] + block[k] = None + # del block[k] return block diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index a2160abcab..fc77da5a88 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -135,6 +135,11 @@ def get_data(self, name, full=True): v["_v_inherit"] = True block["readOnly"] = True v.update(self._resolve_block(v, _seen_blocks)) + # v['_v_original'] = self._resolve_block(v, _seen_blocks) + + for k, v in blocks.items(): + if v.get("s:isVariantOf"): + v['_v_original'] = deepcopy(_seen_blocks[k]) return { "blocks": blocks, From df11630cd91dedf268613ba3c6be68cdaa965b1e Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Tue, 6 Jul 2021 22:28:34 +0300 Subject: [PATCH 67/99] WIP --- src/plone/restapi/slots/__init__.py | 11 +++++++-- src/plone/restapi/slots/interfaces.py | 32 +++++++++++++++++++++------ src/plone/restapi/tests/test_slots.py | 10 ++++----- 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index fc77da5a88..bdbab198e8 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -95,6 +95,7 @@ def get_fills_stack(self, name): def get_data(self, name, full=True): blocks = {} blocks_layout = [] + hidden = [] _replaced = set() _seen_blocks = {} @@ -116,8 +117,13 @@ def get_data(self, name, full=True): if other: _replaced.add(other) + if (not full) and block.get('v:hidden'): + hidden.append(uid) + continue + blocks[uid] = block - if level > 0: + + if level > 0: # anything deeper than "top" level is inherited block["_v_inherit"] = True block["readOnly"] = True @@ -144,7 +150,8 @@ def get_data(self, name, full=True): return { "blocks": blocks, "blocks_layout": { - "items": [b for b in blocks_layout if b in _seen_blocks.keys()] + "items": [b for b in blocks_layout if b in _seen_blocks.keys() + and b not in hidden] }, } diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index fadd6050d4..7e8d92ff33 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -10,6 +10,23 @@ class ISlots(Interface): """Slots are named container of sets of blocks""" + def discover_slots(): + """ Returns a list of all persistent slot names, across the hierarchy""" + + def get_data(name): + """ Get the blocks + blocks_layout for a slot name """ + + def get_editable_slots(): + """ Returns a list of slot names that can be modified by the current principal + """ + + def save_data_to_slot(slot, data): + """ Persist the data for a slot + """ + + def get_fills_stack(): + """ Returns a list of all discovered persistent slots across a hierarchy """ + class ISlotStorage(Interface): """ A store of slots information """ @@ -32,13 +49,14 @@ class ISlotSettings(Interface): """ # special slot fill attributes: -- `s:sameAs`: Ideally blocks uids should be unique across the whole CMS. That's why we + +- `s: sameAs`: Ideally blocks uids should be unique across the whole CMS. That's why we use uuid, right? So, when customizing the order of an inherited slot, a "placeholder" - should be created in the current level. This placeholder would have `s:sameAs` point to + should be created in the current level. This placeholder would have `s: sameAs` point to the "overriden" parent. -- `s:isVariantOf`: An inherited block could be "copied" to another level and mutated - there. `s:isVariantOf` points to the original source block. -- `v:hidden`: An inherited block is hidden at this level. To be used with `s:sameAs` to - point to the "parent" block. When publishing slots (in the `@slots` endpoint) the - hidden blocks are included only when run with `full=true`. +- `s: isVariantOf`: An inherited block could be "copied" to another level and mutated + there. `s: isVariantOf` points to the original source block. +- `v: hidden`: An inherited block is hidden at this level. To be used with `s: sameAs` to + point to the "parent" block. When publishing slots ( in the `@ slots` endpoint) the + hidden blocks are included only when run with `full = true`. """ diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index a3e3736176..1a1713e268 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -197,7 +197,7 @@ def test_slot_stack_deep_with_stack_collapse(self): engine = Slots(obj) - left = engine.get_blocks("left") + left = engine.get_data("left") self.assertEqual( left, { @@ -232,7 +232,7 @@ def test_block_data_gets_inherited(self): obj.slots["left"] = DummySlot.from_data({2: {}}, [2]) engine = Slots(obj) - left = engine.get_blocks("left") + left = engine.get_data("left") self.assertEqual( left, @@ -261,7 +261,7 @@ def test_block_data_gets_override(self): ) engine = Slots(obj) - left = engine.get_blocks("left") + left = engine.get_data("left") self.assertEqual( left, @@ -295,7 +295,7 @@ def test_can_change_order_with_sameOf(self): ) engine = Slots(obj) - left = engine.get_blocks("left") + left = engine.get_data("left") self.assertEqual( left, @@ -333,7 +333,7 @@ def test_can_change_order_from_layout(self): ) engine = Slots(obj) - left = engine.get_blocks("left") + left = engine.get_data("left") self.assertEqual( left, From 8ea93d8de979614ccdc03a8bb547b1e8b29d5c2a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 07:29:48 +0300 Subject: [PATCH 68/99] Add test for block hiding --- src/plone/restapi/slots/__init__.py | 10 ++-- src/plone/restapi/tests/test_slots.py | 67 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index bdbab198e8..6ba2e6def6 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -92,7 +92,7 @@ def get_fills_stack(self, name): return slot_stack - def get_data(self, name, full=True): + def get_data(self, name, full=False): blocks = {} blocks_layout = [] hidden = [] @@ -139,13 +139,13 @@ def get_data(self, name, full=True): for k, v in blocks.items(): if v.get("s:sameAs"): v["_v_inherit"] = True - block["readOnly"] = True + block["readOnly"] = True # TODO: should we set this here? v.update(self._resolve_block(v, _seen_blocks)) # v['_v_original'] = self._resolve_block(v, _seen_blocks) - for k, v in blocks.items(): - if v.get("s:isVariantOf"): - v['_v_original'] = deepcopy(_seen_blocks[k]) + # for k, v in blocks.items(): + # if v.get("s:isVariantOf"): + # v['_v_original'] = deepcopy(_seen_blocks[k]) return { "blocks": blocks, diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 1a1713e268..4a565924c2 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -347,6 +347,73 @@ def test_can_change_order_from_layout(self): }, ) + def test_hide_block(self): + # Hidden blocks are not passed down + root = self.make_content() + + root["documents"].slots["left"] = DummySlot.from_data( + { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + [5, 1, 3], + ) + + obj = root["documents"]["internal"] + obj.slots["left"] = DummySlot.from_data( + { + 2: {"s:isVariantOf": 1, "title": "Second", "v:hidden": True}, + }, + [3, 2], + ) + engine = Slots(obj) + left = engine.get_data("left") + + self.assertEqual( + left, + { + "blocks_layout": {"items": [3, 5]}, + "blocks": { + 3: {"title": "Third", "_v_inherit": True}, + 5: {"title": "Fifth", "_v_inherit": True}, + }, + }, + ) + + def test_hide_blocks_full(self): + # Hidden blocks are not passed down + root = self.make_content() + + root["documents"].slots["left"] = DummySlot.from_data( + { + 1: {"title": "First"}, + 3: {"title": "Third"}, + 5: {"title": "Fifth"}, + }, + [5, 1, 3], + ) + + obj = root["documents"]["internal"] + obj.slots["left"] = DummySlot.from_data( + { + 2: {"s:isVariantOf": 1, "title": "Second", "v:hidden": True}, + }, + [3, 2], + ) + engine = Slots(obj) + left = engine.get_data("left", full=True) + + self.assertEqual( + left, + {'blocks': {2: {'s:isVariantOf': 1, + 'title': 'Second', + 'v:hidden': True}, + 3: {'_v_inherit': True, 'readOnly': True, 'title': 'Third'}, + 5: {'_v_inherit': True, 'readOnly': True, 'title': 'Fifth'}}, + 'blocks_layout': {'items': [3, 2, 5]}}, + ) + def test_save_slots(self): data = { "blocks_layout": {"items": [3, 2, 5]}, From 30c2f9953044403d5d0c54b8532ca2d47b0ea4a6 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 08:09:53 +0300 Subject: [PATCH 69/99] Fix sameAs handling --- src/plone/restapi/slots/__init__.py | 4 ++-- src/plone/restapi/tests/test_slots.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 6ba2e6def6..9c7508d0dd 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -138,9 +138,9 @@ def get_data(self, name, full=False): for k, v in blocks.items(): if v.get("s:sameAs"): - v["_v_inherit"] = True - block["readOnly"] = True # TODO: should we set this here? v.update(self._resolve_block(v, _seen_blocks)) + v["_v_inherit"] = True + v["readOnly"] = True # TODO: should we set this here? # v['_v_original'] = self._resolve_block(v, _seen_blocks) # for k, v in blocks.items(): diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 4a565924c2..d0ab8837c5 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -238,7 +238,7 @@ def test_block_data_gets_inherited(self): left, { "blocks_layout": {"items": [2, 1]}, - "blocks": {1: {"title": "First", "_v_inherit": True}, 2: {}}, + "blocks": {1: {"title": "First", "readOnly": True, "_v_inherit": True}, 2: {}}, }, ) @@ -303,8 +303,8 @@ def test_can_change_order_with_sameOf(self): "blocks_layout": {"items": [4, 2, 5]}, "blocks": { 2: {"title": "Second", "s:isVariantOf": 1}, - 4: {"title": "Third", "s:sameAs": 3, "_v_inherit": True}, - 5: {"title": "Fifth", "_v_inherit": True}, + 4: {"title": "Third", "s:sameAs": 3, "readOnly": True, "_v_inherit": True}, + 5: {"title": "Fifth", "readOnly": True, "_v_inherit": True}, }, }, ) From 527c6212430222fe4542b49b91a88abc9fad4ce0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 08:13:15 +0300 Subject: [PATCH 70/99] Fix some tests --- src/plone/restapi/tests/test_slots.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index d0ab8837c5..e12627e528 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -208,12 +208,15 @@ def test_slot_stack_deep_with_stack_collapse(self): 6: {}, 1: { "_v_inherit": True, + 'readOnly': True, }, 2: { "_v_inherit": True, + 'readOnly': True, }, 3: { "_v_inherit": True, + 'readOnly': True, }, 7: {}, }, @@ -269,7 +272,7 @@ def test_block_data_gets_override(self): "blocks_layout": {"items": [2, 3]}, "blocks": { 2: {"title": "Second", "s:isVariantOf": 1}, - 3: {"title": "Third", "_v_inherit": True}, + 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, }, }, ) @@ -341,8 +344,8 @@ def test_can_change_order_from_layout(self): "blocks_layout": {"items": [3, 2, 5]}, "blocks": { 2: {"title": "Second", "s:isVariantOf": 1}, - 3: {"title": "Third", "_v_inherit": True}, - 5: {"title": "Fifth", "_v_inherit": True}, + 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, + 5: {"title": "Fifth", "_v_inherit": True, 'readOnly': True}, }, }, ) @@ -375,8 +378,8 @@ def test_hide_block(self): { "blocks_layout": {"items": [3, 5]}, "blocks": { - 3: {"title": "Third", "_v_inherit": True}, - 5: {"title": "Fifth", "_v_inherit": True}, + 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, + 5: {"title": "Fifth", "_v_inherit": True, 'readOnly': True}, }, }, ) From 00f515924610480136ae975bc4b82ac41f8fcff7 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 18:14:05 +0300 Subject: [PATCH 71/99] More tests, enable the _v_original as it's needed in frontend when 'unhiding' a saved hidden block --- src/plone/restapi/slots/__init__.py | 8 +- .../restapi/tests/test_services_slots.py | 89 ++++++++++++++++++- 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 9c7508d0dd..a5020467c0 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -143,9 +143,11 @@ def get_data(self, name, full=False): v["readOnly"] = True # TODO: should we set this here? # v['_v_original'] = self._resolve_block(v, _seen_blocks) - # for k, v in blocks.items(): - # if v.get("s:isVariantOf"): - # v['_v_original'] = deepcopy(_seen_blocks[k]) + for k, v in blocks.items(): + if v.get("s:isVariantOf"): + # in the frontend, if we have a block that's hidden then we go and + # "unhide", we'll need the original data for best UX + v['_v_original'] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) return { "blocks": blocks, diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 7c66d54a87..8b3e58d5d7 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -72,10 +72,12 @@ def setup_slots(self): def test_slots_endpoint(self): response = self.api_session.get("/@slots") self.assertEqual(response.status_code, 200) + slots = response.json() self.assertEqual( - response.json(), + slots, { u"@id": u"http://localhost:55001/plone/@slots", + u"can_manage_slots": True, u"items": { u"left": { u"@id": u"http://localhost:55001/plone/@slots/left", @@ -146,3 +148,88 @@ def test_deserializer_on_slot_with_data_and_missing_slots(self): }, ) self.assertEqual(storage["left"].blocks_layout, {"items": [u"1"]}) + + def test_slots_endpoint_hide(self): + + storage = ISlotStorage(self.doc) + storage[u"left"].blocks = {u"2": {"s:isVariantOf": u"1", "v:hidden": True}} + transaction.commit() + + response = self.api_session.get("/folder1/doc11/@slots") + self.assertEqual(response.status_code, 200) + + slots = response.json() + self.assertEqual( + slots, + { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots", + u"can_manage_slots": True, + u"items": { + u"left": { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/left", + u"blocks": { + # 1 is hidden because it's overridden by 2, which is hidden + # u"1": {u"title": u"First"} + u"3": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, + u"5": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + }, + + # in 'doc11' slots, layout is [3, 2], so inherited 5 is at end + u"blocks_layout": {u"items": [u"3", u"5"]}, + u"edit": True, + }, + u"right": { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", + u"blocks": { + u"6": {u"title": u"First", '_v_inherit': True, 'readOnly': True}, + u"7": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, + u"8": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + }, + u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, + u"edit": True, + }, + }, + }, + ) + + def test_slots_endpoint_hide_full(self): + + storage = ISlotStorage(self.doc) + storage[u"left"].blocks = {u"2": {"s:isVariantOf": u"1", "v:hidden": True}} + transaction.commit() + + response = self.api_session.get("/folder1/doc11/@slots?full=true") + self.assertEqual(response.status_code, 200) + + slots = response.json() + + self.assertEqual( + slots, + { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots", + u"can_manage_slots": True, + u"items": { + u"left": { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/left", + u"blocks": { + u'2': {'s:isVariantOf': '1', 'v:hidden': True, + '_v_original': {'title': 'First'}}, + u"3": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, + u"5": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + }, + u"blocks_layout": {u"items": [u"3", u"2", u"5"]}, + u"edit": True, + }, + u"right": { + u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", + u"blocks": { + u"6": {u"title": u"First", '_v_inherit': True, 'readOnly': True}, + u"7": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, + u"8": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + }, + u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, + u"edit": True, + }, + }, + }, + ) From 4061471c1686f9636ef939fc12e24409d3bafc03 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 18:25:46 +0300 Subject: [PATCH 72/99] Fix some tests, run black --- src/plone/restapi/deserializer/blocks.py | 2 +- src/plone/restapi/deserializer/slots.py | 8 +-- src/plone/restapi/serializer/slots.py | 8 +-- src/plone/restapi/services/slots/get.py | 13 ++-- src/plone/restapi/slots/__init__.py | 19 +++--- src/plone/restapi/slots/interfaces.py | 14 ++-- .../restapi/tests/test_services_slots.py | 68 +++++++++++++++---- src/plone/restapi/tests/test_slots.py | 65 ++++++++++++------ 8 files changed, 134 insertions(+), 63 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index f4b132f0c1..bd864a34b5 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -49,7 +49,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1:] + path = path[len(portal_url) + 1 :] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index f4c4e6337b..6e8820db6c 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -27,7 +27,7 @@ @adapter(IContentish, ISlot, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotDeserializer(object): - """ Deserializer of one slot for contentish objects """ + """Deserializer of one slot for contentish objects""" def __init__(self, context, slot, request): self.context = context @@ -87,14 +87,14 @@ def __call__(self, data=None): data["blocks_layout"]["items"] = layout self.slot.blocks_layout = data["blocks_layout"] - self.slot.block_parent = data.get('block_parent', False) + self.slot.block_parent = data.get("block_parent", False) self.slot._p_changed = True @adapter(IPloneSiteRoot, ISlot, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotDeserializerRoot(SlotDeserializer): - """ Deserializer of one slot for site root """ + """Deserializer of one slot for site root""" @adapter(IContentish, ISlotStorage, IBrowserRequest) @@ -138,4 +138,4 @@ def __call__(self, data=None): @adapter(IPloneSiteRoot, ISlotStorage, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotsDeserializerRoot(SlotsDeserializer): - """ Deserializer of slots for site root """ + """Deserializer of slots for site root""" diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index c3b64d6f64..595569ea3a 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -26,11 +26,11 @@ def serialize_data(slot, request, schema): result = {} for name, field in getFields(schema).items(): - if name in ['blocks', 'blocks_layout']: + if name in ["blocks", "blocks_layout"]: continue value = getattr(slot, name, _MISSING) if value is not _MISSING: - result[json_compatible(name)] = value # assumes JSON-compatible values + result[json_compatible(name)] = value # assumes JSON-compatible values return result @@ -81,8 +81,8 @@ def __call__(self, full=False): return result -@ adapter(Interface, ISlotStorage, IBrowserRequest) -@ implementer(ISerializeToJson) +@adapter(Interface, ISlotStorage, IBrowserRequest) +@implementer(ISerializeToJson) class SlotsSerializer(object): """Default slots storage serializer""" diff --git a/src/plone/restapi/services/slots/get.py b/src/plone/restapi/services/slots/get.py index 0e42a4d2a7..27b413e478 100644 --- a/src/plone/restapi/services/slots/get.py +++ b/src/plone/restapi/services/slots/get.py @@ -21,9 +21,9 @@ def is_true(val): if isinstance(val, bool): return val - if val in ['true', 'True', '1', 1]: + if val in ["true", "True", "1", 1]: return True - elif val in ['false', 'False', '0', 0]: + elif val in ["false", "False", "0", 0]: return False return False @@ -59,7 +59,7 @@ def reply(self): adapter = getMultiAdapter( (self.context, storage, self.request), ISerializeToJson ) - is_full = self.request.form.get('full', False) + is_full = self.request.form.get("full", False) result = adapter(is_full) # from plone.restapi.serializer.converters import json_compatible @@ -92,10 +92,11 @@ def replySlot(self): slot.__parent__ = self.storage slot.__name__ = name - result = getMultiAdapter((self.context, slot, self.request), - ISerializeToJson)(self.request.form.get('full', False)) + result = getMultiAdapter((self.context, slot, self.request), ISerializeToJson)( + self.request.form.get("full", False) + ) result["edit"] = name in self.editable_slots - transaction.doom() # avoid writing on read + transaction.doom() # avoid writing on read return result diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index a5020467c0..f7448cce83 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -28,7 +28,7 @@ @adapter(IContentish) @implementer(ISlotStorage) class PersistentSlots(BTreeContainer): - """ Slots container""" + """Slots container""" SlotsStorage = factory(PersistentSlots, SLOTS_KEY) @@ -117,13 +117,13 @@ def get_data(self, name, full=False): if other: _replaced.add(other) - if (not full) and block.get('v:hidden'): + if (not full) and block.get("v:hidden"): hidden.append(uid) continue blocks[uid] = block - if level > 0: # anything deeper than "top" level is inherited + if level > 0: # anything deeper than "top" level is inherited block["_v_inherit"] = True block["readOnly"] = True @@ -133,27 +133,30 @@ def get_data(self, name, full=False): level += 1 - if getattr(slot, 'block_parent', False) and not full: + if getattr(slot, "block_parent", False) and not full: break for k, v in blocks.items(): if v.get("s:sameAs"): v.update(self._resolve_block(v, _seen_blocks)) v["_v_inherit"] = True - v["readOnly"] = True # TODO: should we set this here? + v["readOnly"] = True # TODO: should we set this here? # v['_v_original'] = self._resolve_block(v, _seen_blocks) for k, v in blocks.items(): if v.get("s:isVariantOf"): # in the frontend, if we have a block that's hidden then we go and # "unhide", we'll need the original data for best UX - v['_v_original'] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) + v["_v_original"] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) return { "blocks": blocks, "blocks_layout": { - "items": [b for b in blocks_layout if b in _seen_blocks.keys() - and b not in hidden] + "items": [ + b + for b in blocks_layout + if b in _seen_blocks.keys() and b not in hidden + ] }, } diff --git a/src/plone/restapi/slots/interfaces.py b/src/plone/restapi/slots/interfaces.py index 7e8d92ff33..2fcebba8bb 100644 --- a/src/plone/restapi/slots/interfaces.py +++ b/src/plone/restapi/slots/interfaces.py @@ -11,25 +11,23 @@ class ISlots(Interface): """Slots are named container of sets of blocks""" def discover_slots(): - """ Returns a list of all persistent slot names, across the hierarchy""" + """Returns a list of all persistent slot names, across the hierarchy""" def get_data(name): - """ Get the blocks + blocks_layout for a slot name """ + """Get the blocks + blocks_layout for a slot name""" def get_editable_slots(): - """ Returns a list of slot names that can be modified by the current principal - """ + """Returns a list of slot names that can be modified by the current principal""" def save_data_to_slot(slot, data): - """ Persist the data for a slot - """ + """Persist the data for a slot""" def get_fills_stack(): - """ Returns a list of all discovered persistent slots across a hierarchy """ + """Returns a list of all discovered persistent slots across a hierarchy""" class ISlotStorage(Interface): - """ A store of slots information """ + """A store of slots information""" class ISlot(IBlocks): diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 8b3e58d5d7..2623ea2dd1 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -170,10 +170,17 @@ def test_slots_endpoint_hide(self): u"blocks": { # 1 is hidden because it's overridden by 2, which is hidden # u"1": {u"title": u"First"} - u"3": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, - u"5": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + u"3": { + u"title": u"Third", + "_v_inherit": True, + "readOnly": True, + }, + u"5": { + u"title": u"Fifth", + "_v_inherit": True, + "readOnly": True, + }, }, - # in 'doc11' slots, layout is [3, 2], so inherited 5 is at end u"blocks_layout": {u"items": [u"3", u"5"]}, u"edit": True, @@ -181,9 +188,21 @@ def test_slots_endpoint_hide(self): u"right": { u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", u"blocks": { - u"6": {u"title": u"First", '_v_inherit': True, 'readOnly': True}, - u"7": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, - u"8": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + u"6": { + u"title": u"First", + "_v_inherit": True, + "readOnly": True, + }, + u"7": { + u"title": u"Third", + "_v_inherit": True, + "readOnly": True, + }, + u"8": { + u"title": u"Fifth", + "_v_inherit": True, + "readOnly": True, + }, }, u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, u"edit": True, @@ -212,10 +231,21 @@ def test_slots_endpoint_hide_full(self): u"left": { u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/left", u"blocks": { - u'2': {'s:isVariantOf': '1', 'v:hidden': True, - '_v_original': {'title': 'First'}}, - u"3": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, - u"5": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + u"2": { + "s:isVariantOf": "1", + "v:hidden": True, + "_v_original": {"title": "First"}, + }, + u"3": { + u"title": u"Third", + "_v_inherit": True, + "readOnly": True, + }, + u"5": { + u"title": u"Fifth", + "_v_inherit": True, + "readOnly": True, + }, }, u"blocks_layout": {u"items": [u"3", u"2", u"5"]}, u"edit": True, @@ -223,9 +253,21 @@ def test_slots_endpoint_hide_full(self): u"right": { u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", u"blocks": { - u"6": {u"title": u"First", '_v_inherit': True, 'readOnly': True}, - u"7": {u"title": u"Third", '_v_inherit': True, 'readOnly': True}, - u"8": {u"title": u"Fifth", '_v_inherit': True, 'readOnly': True}, + u"6": { + u"title": u"First", + "_v_inherit": True, + "readOnly": True, + }, + u"7": { + u"title": u"Third", + "_v_inherit": True, + "readOnly": True, + }, + u"8": { + u"title": u"Fifth", + "_v_inherit": True, + "readOnly": True, + }, }, u"blocks_layout": {u"items": [u"8", u"6", u"7"]}, u"edit": True, diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index e12627e528..fabb6df522 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -208,15 +208,15 @@ def test_slot_stack_deep_with_stack_collapse(self): 6: {}, 1: { "_v_inherit": True, - 'readOnly': True, + "readOnly": True, }, 2: { "_v_inherit": True, - 'readOnly': True, + "readOnly": True, }, 3: { "_v_inherit": True, - 'readOnly': True, + "readOnly": True, }, 7: {}, }, @@ -241,7 +241,10 @@ def test_block_data_gets_inherited(self): left, { "blocks_layout": {"items": [2, 1]}, - "blocks": {1: {"title": "First", "readOnly": True, "_v_inherit": True}, 2: {}}, + "blocks": { + 1: {"title": "First", "readOnly": True, "_v_inherit": True}, + 2: {}, + }, }, ) @@ -271,8 +274,12 @@ def test_block_data_gets_override(self): { "blocks_layout": {"items": [2, 3]}, "blocks": { - 2: {"title": "Second", "s:isVariantOf": 1}, - 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, + 2: { + "title": "Second", + "s:isVariantOf": 1, + "_v_original": {"title": "First"}, + }, + 3: {"title": "Third", "_v_inherit": True, "readOnly": True}, }, }, ) @@ -305,8 +312,17 @@ def test_can_change_order_with_sameOf(self): { "blocks_layout": {"items": [4, 2, 5]}, "blocks": { - 2: {"title": "Second", "s:isVariantOf": 1}, - 4: {"title": "Third", "s:sameAs": 3, "readOnly": True, "_v_inherit": True}, + 2: { + "title": "Second", + "s:isVariantOf": 1, + "_v_original": {"title": "First"}, + }, + 4: { + "title": "Third", + "s:sameAs": 3, + "readOnly": True, + "_v_inherit": True, + }, 5: {"title": "Fifth", "readOnly": True, "_v_inherit": True}, }, }, @@ -343,9 +359,13 @@ def test_can_change_order_from_layout(self): { "blocks_layout": {"items": [3, 2, 5]}, "blocks": { - 2: {"title": "Second", "s:isVariantOf": 1}, - 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, - 5: {"title": "Fifth", "_v_inherit": True, 'readOnly': True}, + 2: { + "title": "Second", + "s:isVariantOf": 1, + "_v_original": {"title": "First"}, + }, + 3: {"title": "Third", "_v_inherit": True, "readOnly": True}, + 5: {"title": "Fifth", "_v_inherit": True, "readOnly": True}, }, }, ) @@ -378,8 +398,8 @@ def test_hide_block(self): { "blocks_layout": {"items": [3, 5]}, "blocks": { - 3: {"title": "Third", "_v_inherit": True, 'readOnly': True}, - 5: {"title": "Fifth", "_v_inherit": True, 'readOnly': True}, + 3: {"title": "Third", "_v_inherit": True, "readOnly": True}, + 5: {"title": "Fifth", "_v_inherit": True, "readOnly": True}, }, }, ) @@ -409,12 +429,19 @@ def test_hide_blocks_full(self): self.assertEqual( left, - {'blocks': {2: {'s:isVariantOf': 1, - 'title': 'Second', - 'v:hidden': True}, - 3: {'_v_inherit': True, 'readOnly': True, 'title': 'Third'}, - 5: {'_v_inherit': True, 'readOnly': True, 'title': 'Fifth'}}, - 'blocks_layout': {'items': [3, 2, 5]}}, + { + "blocks": { + 2: { + "s:isVariantOf": 1, + "title": "Second", + "v:hidden": True, + "_v_original": {"title": "First"}, + }, + 3: {"_v_inherit": True, "readOnly": True, "title": "Third"}, + 5: {"_v_inherit": True, "readOnly": True, "title": "Fifth"}, + }, + "blocks_layout": {"items": [3, 2, 5]}, + }, ) def test_save_slots(self): From 33a0d8f0bcc348d364bc6e9c410be125433358e2 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 19:03:04 +0300 Subject: [PATCH 73/99] Fix deserializer test --- src/plone/restapi/tests/http-examples/registry_get_list.resp | 2 +- src/plone/restapi/tests/test_deserializer_slots.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/tests/http-examples/registry_get_list.resp b/src/plone/restapi/tests/http-examples/registry_get_list.resp index e89ba0cc3c..6eb76e915c 100644 --- a/src/plone/restapi/tests/http-examples/registry_get_list.resp +++ b/src/plone/restapi/tests/http-examples/registry_get_list.resp @@ -434,5 +434,5 @@ Content-Type: application/json "value": false } ], - "items_total": 1779 + "items_total": 1780 } \ No newline at end of file diff --git a/src/plone/restapi/tests/test_deserializer_slots.py b/src/plone/restapi/tests/test_deserializer_slots.py index 3424130d66..fd352b673c 100644 --- a/src/plone/restapi/tests/test_deserializer_slots.py +++ b/src/plone/restapi/tests/test_deserializer_slots.py @@ -279,5 +279,5 @@ def test_delete_in_parent_affects_child(self): engine = ISlots(self.doc) self.assertEqual( - engine.get_blocks("left")["blocks_layout"]["items"], [u"1", u"3"] + engine.get_data("left", full=True)["blocks_layout"]["items"], [u"1", u"3"] ) From 3c6cfa4e8946a252dd19de80718088683d0785d8 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 19:14:41 +0300 Subject: [PATCH 74/99] Fix serializer test --- .../restapi/tests/test_serializer_slots.py | 60 ++++++++++++++----- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/plone/restapi/tests/test_serializer_slots.py b/src/plone/restapi/tests/test_serializer_slots.py index f582d3885a..1671cfdcf1 100644 --- a/src/plone/restapi/tests/test_serializer_slots.py +++ b/src/plone/restapi/tests/test_serializer_slots.py @@ -101,9 +101,9 @@ def test_slot_deep(self): { "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", "blocks": { - 1: {u"_v_inherit": True}, - 2: {u"_v_inherit": True}, - 3: {u"_v_inherit": True}, + 1: {u"_v_inherit": True, "readOnly": True}, + 2: {u"_v_inherit": True, "readOnly": True}, + 3: {u"_v_inherit": True, "readOnly": True}, }, "blocks_layout": {"items": [1, 2, 3]}, }, @@ -134,8 +134,12 @@ def test_data_override_with_isVariant(self): { "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", "blocks": { - 2: {u"s:isVariantOf": 1, u"title": u"Second"}, - 3: {u"_v_inherit": True, u"title": u"Third"}, + 2: { + "_v_original": {"title": "First"}, + u"s:isVariantOf": 1, + u"title": u"Second", + }, + 3: {"readOnly": True, u"_v_inherit": True, u"title": u"Third"}, }, "blocks_layout": {"items": [2, 3]}, }, @@ -167,9 +171,13 @@ def test_change_order_from_layout(self): { "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", "blocks": { - 2: {u"s:isVariantOf": 1, u"title": u"Second"}, - 3: {u"_v_inherit": True, u"title": u"Third"}, - 5: {u"_v_inherit": True, u"title": u"Fifth"}, + 2: { + u"s:isVariantOf": 1, + u"title": u"Second", + "_v_original": {"title": "First"}, + }, + 3: {u"_v_inherit": True, u"title": u"Third", "readOnly": True}, + 5: {u"_v_inherit": True, u"title": u"Fifth", "readOnly": True}, }, "blocks_layout": {"items": [3, 2, 5]}, }, @@ -217,18 +225,42 @@ def test_serialize_storage(self): u"left": { "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/left", "blocks": { - 2: {u"s:isVariantOf": 1, u"title": u"Second"}, - 3: {u"_v_inherit": True, u"title": u"Third"}, - 5: {u"_v_inherit": True, u"title": u"Fifth"}, + 2: { + u"s:isVariantOf": 1, + u"title": u"Second", + "_v_original": {"title": "First"}, + }, + 3: { + u"_v_inherit": True, + u"title": u"Third", + "readOnly": True, + }, + 5: { + u"_v_inherit": True, + u"title": u"Fifth", + "readOnly": True, + }, }, "blocks_layout": {"items": [3, 2, 5]}, }, u"right": { "@id": "http://nohost/plone/documents/company-a/doc-1/@slots/right", "blocks": { - 6: {u"title": u"First", u"_v_inherit": True}, - 7: {u"title": u"Third", u"_v_inherit": True}, - 8: {u"title": u"Fifth", u"_v_inherit": True}, + 6: { + u"title": u"First", + u"_v_inherit": True, + "readOnly": True, + }, + 7: { + u"title": u"Third", + u"_v_inherit": True, + "readOnly": True, + }, + 8: { + u"title": u"Fifth", + u"_v_inherit": True, + "readOnly": True, + }, }, "blocks_layout": {"items": [8, 6, 7]}, }, From 4964317c70587f0b36638c336e4401106b5f2391 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 19:26:30 +0300 Subject: [PATCH 75/99] Fix one test in test_addons --- src/plone/restapi/tests/test_addons.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plone/restapi/tests/test_addons.py b/src/plone/restapi/tests/test_addons.py index d607bd49ae..e402bcc4e8 100644 --- a/src/plone/restapi/tests/test_addons.py +++ b/src/plone/restapi/tests/test_addons.py @@ -134,7 +134,7 @@ def _get_upgrade_info(self): "available": True, "hasProfile": True, "installedVersion": "0002", - "newVersion": "0006", + "newVersion": "0007", "required": True, }, _get_upgrade_info(self), @@ -148,8 +148,8 @@ def _get_upgrade_info(self): { "available": False, "hasProfile": True, - "installedVersion": "0006", - "newVersion": "0006", + "installedVersion": "0007", + "newVersion": "0007", "required": False, }, _get_upgrade_info(self), From f729ae005c769fb072d72644f13dcc597457c4db Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 19:54:50 +0300 Subject: [PATCH 76/99] Add upgrade handler --- src/plone/restapi/upgrades/configure.zcml | 18 ++++++++++++++++++ src/plone/restapi/upgrades/to0007.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/plone/restapi/upgrades/to0007.py diff --git a/src/plone/restapi/upgrades/configure.zcml b/src/plone/restapi/upgrades/configure.zcml index 66bb67b0d7..0e39610c6c 100644 --- a/src/plone/restapi/upgrades/configure.zcml +++ b/src/plone/restapi/upgrades/configure.zcml @@ -69,4 +69,22 @@ handler="plone.restapi.upgrades.to0006.rename_iface_to_name_in_blocks_behavior" /> + + + + diff --git a/src/plone/restapi/upgrades/to0007.py b/src/plone/restapi/upgrades/to0007.py new file mode 100644 index 0000000000..10814617e3 --- /dev/null +++ b/src/plone/restapi/upgrades/to0007.py @@ -0,0 +1,22 @@ +from plone import api + + +def slots_configuration(setup_context): + setup_context.runImportStepFromProfile( + "profile-plone.restapi.upgrades:0007", + "rolemap", + run_dependencies=False, + purge_old=False, + ) + setup_context.runImportStepFromProfile( + "profile-plone.restapi.upgrades:0007", + "plone.app.registry", + run_dependencies=False, + purge_old=False, + ) + setup_context.runImportStepFromProfile( + "profile-plone.restapi.upgrades:0007", + "controlpanel", + run_dependencies=False, + purge_old=False, + ) From 8fb791d1782c15ab6a8160dc7767e6dfeff51d33 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 8 Jul 2021 20:01:42 +0300 Subject: [PATCH 77/99] Remove unused import --- src/plone/restapi/upgrades/to0007.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/plone/restapi/upgrades/to0007.py b/src/plone/restapi/upgrades/to0007.py index 10814617e3..d22c290914 100644 --- a/src/plone/restapi/upgrades/to0007.py +++ b/src/plone/restapi/upgrades/to0007.py @@ -1,6 +1,3 @@ -from plone import api - - def slots_configuration(setup_context): setup_context.runImportStepFromProfile( "profile-plone.restapi.upgrades:0007", From 4ce5fde13a5adc4dea57f3f1b9682c9fe844cc54 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 10 Jul 2021 09:36:10 +0300 Subject: [PATCH 78/99] Fix _v_ handling --- src/plone/restapi/deserializer/blocks.py | 9 ++++----- src/plone/restapi/deserializer/slots.py | 4 ---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index 70b390e9c7..07fb4d53dd 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -46,7 +46,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1 :] + path = path[len(portal_url) + 1:] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") @@ -236,10 +236,9 @@ def __init__(self, context, request): self.request = request def __call__(self, block): - for k, v in block.items(): - if k.startswith("_v_"): - block[k] = None - # del block[k] + keys = [k for k in block.keys() if k.startswith('_v_')] + for k in keys: + del block[k] return block diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 6e8820db6c..da2cf12761 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -20,10 +20,6 @@ import copy -# from plone.restapi.events import BlocksRemovedEvent -# from zope.event import notify - - @adapter(IContentish, ISlot, IBrowserRequest) @implementer(IDeserializeFromJson) class SlotDeserializer(object): From 8aea2f9dc96a277231679a4ea8ec003ab3b199ad Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 10 Jul 2021 19:22:30 +0300 Subject: [PATCH 79/99] Fix deserializing when showing a hidden parent --- src/plone/restapi/deserializer/slots.py | 8 +++--- src/plone/restapi/slots/__init__.py | 34 +++++++++++++------------ src/plone/restapi/slots/indexers.py | 20 --------------- 3 files changed, 23 insertions(+), 39 deletions(-) delete mode 100644 src/plone/restapi/slots/indexers.py diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index da2cf12761..7d87a7818e 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -15,6 +15,7 @@ from zope.component import getMultiAdapter from zope.component import subscribers from zope.interface import implementer +from zope.location.interfaces import ILocation from zope.publisher.interfaces.browser import IBrowserRequest import copy @@ -39,9 +40,10 @@ def __call__(self, data=None): incoming_blocks = copy.deepcopy(data["blocks"]) - engine = ISlots(self.context) - all_blocks_ids = engine.get_data(self.slot.__name__, full=True)["blocks"].keys() - parent_block_ids = list(set(all_blocks_ids) - set(self.slot.blocks.keys())) + parent = ILocation(self.context).__parent__ + engine = ISlots(parent) + parent_block_ids = list(engine.get_data(self.slot.__name__, + full=True)['blocks'].keys()) # don't keep blocks that are not in incoming data for k in list(self.slot.blocks.keys()): diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index f7448cce83..816f3ec067 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -93,12 +93,12 @@ def get_fills_stack(self, name): return slot_stack def get_data(self, name, full=False): - blocks = {} - blocks_layout = [] - hidden = [] - - _replaced = set() - _seen_blocks = {} + _blocks = {} # the resulting blocks + _blocks_layout = [] # a tentative block_layout ordered list + _hidden = [] # list of block uids that are hidden + _seen_blocks = {} # all blocks in this hierarchy + _replaced = set() # original blocks that are overridden by variants. We + # don't want to include these in the final output stack = self.get_fills_stack(name) @@ -112,50 +112,52 @@ def get_data(self, name, full=False): block = deepcopy(block) _seen_blocks[uid] = block - if not (uid in blocks or uid in _replaced): + if not (uid in _blocks or uid in _replaced): other = block.get("s:isVariantOf") or block.get("s:sameAs") if other: _replaced.add(other) if (not full) and block.get("v:hidden"): - hidden.append(uid) + _hidden.append(uid) continue - blocks[uid] = block + _blocks[uid] = block if level > 0: # anything deeper than "top" level is inherited block["_v_inherit"] = True block["readOnly"] = True for uid in slot.blocks_layout["items"]: - if not (uid in blocks_layout or uid in _replaced): - blocks_layout.append(uid) + if not (uid in _blocks_layout or uid in _replaced): + _blocks_layout.append(uid) level += 1 if getattr(slot, "block_parent", False) and not full: break - for k, v in blocks.items(): + for k, v in _blocks.items(): if v.get("s:sameAs"): v.update(self._resolve_block(v, _seen_blocks)) v["_v_inherit"] = True v["readOnly"] = True # TODO: should we set this here? # v['_v_original'] = self._resolve_block(v, _seen_blocks) - for k, v in blocks.items(): + for k, v in _blocks.items(): if v.get("s:isVariantOf"): # in the frontend, if we have a block that's hidden then we go and # "unhide", we'll need the original data for best UX + + # TODO: what do do when the inherited block has been deleted? v["_v_original"] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) return { - "blocks": blocks, + "blocks": _blocks, "blocks_layout": { "items": [ b - for b in blocks_layout - if b in _seen_blocks.keys() and b not in hidden + for b in _blocks_layout + if b in _seen_blocks.keys() and b not in _hidden ] }, } diff --git a/src/plone/restapi/slots/indexers.py b/src/plone/restapi/slots/indexers.py deleted file mode 100644 index 04aefe8dae..0000000000 --- a/src/plone/restapi/slots/indexers.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -from plone.indexer.decorator import indexer -from plone.restapi.slots import SLOTS_KEY -from plone.restapi.slots.interfaces import ISlotStorage -from Products.CMFCore.interfaces import IContentish -from zope.annotation.interfaces import IAnnotations - - -@indexer(IContentish) -def slot_block_ids(obj): - if SLOTS_KEY not in IAnnotations(obj): - return - - blocks = [] - storage = ISlotStorage(obj) - for name, slot in storage.items(): - blocks.extend(slot.blocks_layout["items"]) - - return blocks From fa3c80f7b2e11fcd949b5383af04c88319b99db2 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 10 Jul 2021 19:49:48 +0300 Subject: [PATCH 80/99] Fix parent if None --- src/plone/restapi/deserializer/slots.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 7d87a7818e..085502bbba 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -40,10 +40,12 @@ def __call__(self, data=None): incoming_blocks = copy.deepcopy(data["blocks"]) + parent_block_ids = [] parent = ILocation(self.context).__parent__ - engine = ISlots(parent) - parent_block_ids = list(engine.get_data(self.slot.__name__, - full=True)['blocks'].keys()) + if parent is not None: + engine = ISlots(parent) + parent_block_ids = list(engine.get_data(self.slot.__name__, + full=True)['blocks'].keys()) # don't keep blocks that are not in incoming data for k in list(self.slot.blocks.keys()): From 222043b3c6a7b01abe13c5e548084edd652f73f2 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sat, 10 Jul 2021 19:59:53 +0300 Subject: [PATCH 81/99] Run black, localize test urls --- src/plone/restapi/deserializer/blocks.py | 4 +- src/plone/restapi/deserializer/slots.py | 5 ++- src/plone/restapi/slots/__init__.py | 10 ++--- .../restapi/tests/test_services_slots.py | 42 ++++++++++++++----- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/plone/restapi/deserializer/blocks.py b/src/plone/restapi/deserializer/blocks.py index 07fb4d53dd..8f08e00a22 100644 --- a/src/plone/restapi/deserializer/blocks.py +++ b/src/plone/restapi/deserializer/blocks.py @@ -46,7 +46,7 @@ def path2uid(context, link): context_url = context.absolute_url() relative_up = len(context_url.split("/")) - len(portal_url.split("/")) if path.startswith(portal_url): - path = path[len(portal_url) + 1:] + path = path[len(portal_url) + 1 :] if not path.startswith(portal_path): path = "{portal_path}/{path}".format( portal_path=portal_path, path=path.lstrip("/") @@ -236,7 +236,7 @@ def __init__(self, context, request): self.request = request def __call__(self, block): - keys = [k for k in block.keys() if k.startswith('_v_')] + keys = [k for k in block.keys() if k.startswith("_v_")] for k in keys: del block[k] diff --git a/src/plone/restapi/deserializer/slots.py b/src/plone/restapi/deserializer/slots.py index 085502bbba..718d47d92c 100644 --- a/src/plone/restapi/deserializer/slots.py +++ b/src/plone/restapi/deserializer/slots.py @@ -44,8 +44,9 @@ def __call__(self, data=None): parent = ILocation(self.context).__parent__ if parent is not None: engine = ISlots(parent) - parent_block_ids = list(engine.get_data(self.slot.__name__, - full=True)['blocks'].keys()) + parent_block_ids = list( + engine.get_data(self.slot.__name__, full=True)["blocks"].keys() + ) # don't keep blocks that are not in incoming data for k in list(self.slot.blocks.keys()): diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 816f3ec067..f53e6b676d 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -93,11 +93,11 @@ def get_fills_stack(self, name): return slot_stack def get_data(self, name, full=False): - _blocks = {} # the resulting blocks - _blocks_layout = [] # a tentative block_layout ordered list - _hidden = [] # list of block uids that are hidden - _seen_blocks = {} # all blocks in this hierarchy - _replaced = set() # original blocks that are overridden by variants. We + _blocks = {} # the resulting blocks + _blocks_layout = [] # a tentative block_layout ordered list + _hidden = [] # list of block uids that are hidden + _seen_blocks = {} # all blocks in this hierarchy + _replaced = set() # original blocks that are overridden by variants. We # don't want to include these in the final output stack = self.get_fills_stack(name) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 2623ea2dd1..71e2471f5e 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -73,14 +73,18 @@ def test_slots_endpoint(self): response = self.api_session.get("/@slots") self.assertEqual(response.status_code, 200) slots = response.json() + + def url(s): + return s.replace("http://localhost:55001/plone", self.portal.absolute_url()) + self.assertEqual( slots, { - u"@id": u"http://localhost:55001/plone/@slots", + u"@id": url(u"http://localhost:55001/plone/@slots"), u"can_manage_slots": True, u"items": { u"left": { - u"@id": u"http://localhost:55001/plone/@slots/left", + u"@id": url(u"http://localhost:55001/plone/@slots/left"), u"blocks": { u"1": {u"title": u"First"}, u"3": {u"title": u"Third"}, @@ -90,7 +94,7 @@ def test_slots_endpoint(self): u"edit": True, }, u"right": { - u"@id": u"http://localhost:55001/plone/@slots/right", + u"@id": url(u"http://localhost:55001/plone/@slots/right"), u"blocks": { u"6": {u"title": u"First"}, u"7": {u"title": u"Third"}, @@ -110,10 +114,14 @@ def test_slot_endpoint(self): def test_slot_endpoint_on_root(self): response = self.api_session.get("/@slots/left") self.assertEqual(response.status_code, 200) + + def url(s): + return s.replace("http://localhost:55001/plone", self.portal.absolute_url()) + self.assertEqual( response.json(), { - u"@id": u"http://localhost:55001/plone/@slots/left", + u"@id": url(u"http://localhost:55001/plone/@slots/left"), u"edit": True, u"blocks": { u"1": {u"title": u"First"}, @@ -158,15 +166,20 @@ def test_slots_endpoint_hide(self): response = self.api_session.get("/folder1/doc11/@slots") self.assertEqual(response.status_code, 200) + def url(s): + return s.replace("http://localhost:55001/plone", self.portal.absolute_url()) + slots = response.json() self.assertEqual( slots, { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots", + u"@id": url(u"http://localhost:55001/plone/folder1/doc11/@slots"), u"can_manage_slots": True, u"items": { u"left": { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/left", + u"@id": url( + u"http://localhost:55001/plone/folder1/doc11/@slots/left" + ), u"blocks": { # 1 is hidden because it's overridden by 2, which is hidden # u"1": {u"title": u"First"} @@ -186,7 +199,9 @@ def test_slots_endpoint_hide(self): u"edit": True, }, u"right": { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", + u"@id": url( + u"http://localhost:55001/plone/folder1/doc11/@slots/right" + ), u"blocks": { u"6": { u"title": u"First", @@ -222,14 +237,19 @@ def test_slots_endpoint_hide_full(self): slots = response.json() + def url(s): + return s.replace("http://localhost:55001/plone", self.portal.absolute_url()) + self.assertEqual( slots, { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots", + u"@id": url(u"http://localhost:55001/plone/folder1/doc11/@slots"), u"can_manage_slots": True, u"items": { u"left": { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/left", + u"@id": url( + u"http://localhost:55001/plone/folder1/doc11/@slots/left" + ), u"blocks": { u"2": { "s:isVariantOf": "1", @@ -251,7 +271,9 @@ def test_slots_endpoint_hide_full(self): u"edit": True, }, u"right": { - u"@id": u"http://localhost:55001/plone/folder1/doc11/@slots/right", + u"@id": url( + u"http://localhost:55001/plone/folder1/doc11/@slots/right" + ), u"blocks": { u"6": { u"title": u"First", From ecf078886e7bb4a589ec69de1b6d94291229bce0 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 11 Jul 2021 08:55:26 +0300 Subject: [PATCH 82/99] Add some test descriptions --- src/plone/restapi/tests/test_services_slots.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/tests/test_services_slots.py b/src/plone/restapi/tests/test_services_slots.py index 71e2471f5e..5ad1d0ae09 100644 --- a/src/plone/restapi/tests/test_services_slots.py +++ b/src/plone/restapi/tests/test_services_slots.py @@ -70,6 +70,7 @@ def setup_slots(self): storage[u"left"].blocks_layout = {"items": [u"3", u"2"]} def test_slots_endpoint(self): + # generic /@slots endpoint call response = self.api_session.get("/@slots") self.assertEqual(response.status_code, 200) slots = response.json() @@ -107,11 +108,13 @@ def url(s): }, ) - def test_slot_endpoint(self): + def test_slot_endpoint_missing(self): + # @slot endpoint call on an slot name that's not found in the hierarchy response = self.api_session.get("/@slots/unregistered") self.assertEqual(response.status_code, 404) def test_slot_endpoint_on_root(self): + # @slot/ endpoint call to retrieve a specific slot response = self.api_session.get("/@slots/left") self.assertEqual(response.status_code, 200) @@ -133,10 +136,13 @@ def url(s): ) def test_deserializer_on_slot(self): + # Plone site can host slots response = self.api_session.patch("/@slots/left", json={}) self.assertEqual(response.status_code, 204) def test_deserializer_on_slot_with_data_and_missing_slots(self): + # slot engine cleans up blocks with missing definition (not inherited, not + # defined at current level) response = self.api_session.patch( "/@slots/left", json={ @@ -158,6 +164,7 @@ def test_deserializer_on_slot_with_data_and_missing_slots(self): self.assertEqual(storage["left"].blocks_layout, {"items": [u"1"]}) def test_slots_endpoint_hide(self): + # engine can omit some blocks from response if the inherited blocks are "hidden" storage = ISlotStorage(self.doc) storage[u"left"].blocks = {u"2": {"s:isVariantOf": u"1", "v:hidden": True}} @@ -227,6 +234,7 @@ def url(s): ) def test_slots_endpoint_hide_full(self): + # slots engine will include the hidden blocks if passed with full=true storage = ISlotStorage(self.doc) storage[u"left"].blocks = {u"2": {"s:isVariantOf": u"1", "v:hidden": True}} From d8eaaae3ddf7e69a699754f8d0c34eae9dc49b9a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 11 Jul 2021 21:34:31 +0300 Subject: [PATCH 83/99] Add a test for third-hand inherited slots --- src/plone/restapi/tests/test_slots.py | 65 +++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index fabb6df522..b61d13146b 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -444,6 +444,69 @@ def test_hide_blocks_full(self): }, ) + def test_get_data_multiple_inherit(self): + s0 = [ + {"F": {"@type": "text", "text": "level 0"}}, + ["F"], + ] # the original block + s1 = [ + { + "D": {"@type": "text", "text": "local 2"}, + "B": {"@type": "text", "text": "local 1"}, + "E": { # customizes the original + "@type": "text", + "s:isVariantOf": "F", + "text": "right customized", + }, + }, + ["B", "E", "D"], + ] + s2 = [ + { + "A": {"@type": "text", "text": "local 3"}, + "C": { # customizes the customized version by hiding it + "@type": "text", + "readOnly": True, + "s:isVariantOf": "E", + "text": "right customized", + "v:hidden": True, + }, + }, + ["B", "A", "C", "D"], + ] + + root = self.make_content() + root.slots["left"] = DummySlot.from_data(*s0) + root["documents"].slots["left"] = DummySlot.from_data(*s1) + root["documents"]["internal"].slots["left"] = DummySlot.from_data(*s2) + + engine = Slots(root["documents"]["internal"]) + left = engine.get_data("left", full=True) + self.assertEqual(left.blocks, + {'A': {'@type': 'text', 'text': 'local 3'}, + 'B': {'@type': 'text', + '_v_inherit': True, + 'readOnly': True, + 'text': 'local 1'}, + 'C': {'@type': 'text', + '_v_original': {'@type': 'text', + 's:isVariantOf': 'F', + 'text': 'right customized'}, + 'readOnly': True, + 's:isVariantOf': 'E', + 'text': 'right customized', + 'v:hidden': True}, + 'D': {'@type': 'text', + '_v_inherit': True, + 'readOnly': True, + 'text': 'local 2'}, + 'F': {'@type': 'text', + '_v_inherit': True, + 'readOnly': True, + 'text': 'level 0'}}) + # F should not be in layout, as it's "third-hand" inherited + self.assertEqual(left['blocks_layout'], {'items': ['B', 'A', 'C', 'D', 'F']}) + def test_save_slots(self): data = { "blocks_layout": {"items": [3, 2, 5]}, @@ -550,6 +613,7 @@ def test_editable_slots_as_manager(self): self.assertEqual(left, ["left"]) def test_editable_slots_as_member(self): + # simple member cannot edit slots self.login("simple_member") engine = ISlots(self.doc) @@ -570,6 +634,7 @@ def test_editable_slots_as_member(self): self.assertEqual(engine.get_editable_slots(), []) def test_editable_slots_as_editor(self): + # a user with "Modify portal content" can edit "content" slots self.login("editor_member") engine = ISlots(self.doc) From 211420e8a02180c2242ac02ae42e444df304beca Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 11 Jul 2021 21:54:16 +0300 Subject: [PATCH 84/99] Make the test more clear --- src/plone/restapi/tests/test_slots.py | 74 ++++++++++++++++----------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index b61d13146b..d005032856 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -448,31 +448,31 @@ def test_get_data_multiple_inherit(self): s0 = [ {"F": {"@type": "text", "text": "level 0"}}, ["F"], - ] # the original block + ] # the original block s1 = [ { "D": {"@type": "text", "text": "local 2"}, "B": {"@type": "text", "text": "local 1"}, - "E": { # customizes the original + "F-E": { # customizes the original "@type": "text", "s:isVariantOf": "F", "text": "right customized", }, }, - ["B", "E", "D"], + ["B", "F-E", "D"], ] s2 = [ { "A": {"@type": "text", "text": "local 3"}, - "C": { # customizes the customized version by hiding it + "F-E-C": { # customizes the customized version by hiding it "@type": "text", "readOnly": True, - "s:isVariantOf": "E", + "s:isVariantOf": "F-E", "text": "right customized", "v:hidden": True, }, }, - ["B", "A", "C", "D"], + ["B", "A", "F-E-C", "D"], ] root = self.make_content() @@ -482,30 +482,46 @@ def test_get_data_multiple_inherit(self): engine = Slots(root["documents"]["internal"]) left = engine.get_data("left", full=True) - self.assertEqual(left.blocks, - {'A': {'@type': 'text', 'text': 'local 3'}, - 'B': {'@type': 'text', - '_v_inherit': True, - 'readOnly': True, - 'text': 'local 1'}, - 'C': {'@type': 'text', - '_v_original': {'@type': 'text', - 's:isVariantOf': 'F', - 'text': 'right customized'}, - 'readOnly': True, - 's:isVariantOf': 'E', - 'text': 'right customized', - 'v:hidden': True}, - 'D': {'@type': 'text', - '_v_inherit': True, - 'readOnly': True, - 'text': 'local 2'}, - 'F': {'@type': 'text', - '_v_inherit': True, - 'readOnly': True, - 'text': 'level 0'}}) + self.assertEqual( + left["blocks"], + { + "A": {"@type": "text", "text": "local 3"}, + "B": { + "@type": "text", + "_v_inherit": True, + "readOnly": True, + "text": "local 1", + }, + "F-E-C": { + "@type": "text", + "_v_original": { + "@type": "text", + "s:isVariantOf": "F", + "text": "right customized", + }, + "readOnly": True, + "s:isVariantOf": "F-E", + "text": "right customized", + "v:hidden": True, + }, + "D": { + "@type": "text", + "_v_inherit": True, + "readOnly": True, + "text": "local 2", + }, + "F": { + "@type": "text", + "_v_inherit": True, + "readOnly": True, + "text": "level 0", + }, + }, + ) # F should not be in layout, as it's "third-hand" inherited - self.assertEqual(left['blocks_layout'], {'items': ['B', 'A', 'C', 'D', 'F']}) + self.assertEqual( + left["blocks_layout"], {"items": ["B", "A", "F-E-C", "D", "F"]} + ) def test_save_slots(self): data = { From 1bfe28fd4dc9214aab2135759183fd0ff5fcdd9d Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 11 Jul 2021 23:06:39 +0300 Subject: [PATCH 85/99] WIP on test --- src/plone/restapi/slots/__init__.py | 33 +++++++++++++-------------- src/plone/restapi/tests/test_slots.py | 11 ++++----- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index f53e6b676d..37b921110d 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -100,39 +100,40 @@ def get_data(self, name, full=False): _replaced = set() # original blocks that are overridden by variants. We # don't want to include these in the final output + __to_include_originals = [] # these blocks need to reference their original + stack = self.get_fills_stack(name) - level = 0 - for slot in stack: + for level, slot in enumerate(stack): if slot is None: - level += 1 continue for uid, block in slot.blocks.items(): block = deepcopy(block) _seen_blocks[uid] = block - if not (uid in _blocks or uid in _replaced): + if not (uid in _blocks): # or uid in _replaced other = block.get("s:isVariantOf") or block.get("s:sameAs") if other: _replaced.add(other) + __to_include_originals.append(block) - if (not full) and block.get("v:hidden"): - _hidden.append(uid) + if block.get("v:hidden") and (not full): + _hidden.append(uid) # we exclude hidden blocks continue - _blocks[uid] = block - if level > 0: # anything deeper than "top" level is inherited block["_v_inherit"] = True block["readOnly"] = True + if uid not in _replaced: + _blocks[uid] = block + for uid in slot.blocks_layout["items"]: if not (uid in _blocks_layout or uid in _replaced): + # inherited blocks are placed at the end _blocks_layout.append(uid) - level += 1 - if getattr(slot, "block_parent", False) and not full: break @@ -143,13 +144,11 @@ def get_data(self, name, full=False): v["readOnly"] = True # TODO: should we set this here? # v['_v_original'] = self._resolve_block(v, _seen_blocks) - for k, v in _blocks.items(): - if v.get("s:isVariantOf"): - # in the frontend, if we have a block that's hidden then we go and - # "unhide", we'll need the original data for best UX - - # TODO: what do do when the inherited block has been deleted? - v["_v_original"] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) + # in the frontend, if we have a block that's hidden then we go and + # "unhide", we'll need the original data for best UX + # TODO: what do do when the inherited block has been deleted? + for v in __to_include_originals: + v["_v_original"] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) return { "blocks": _blocks, diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index d005032856..1536321575 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -444,6 +444,8 @@ def test_hide_blocks_full(self): }, ) + maxDiff = None + def test_get_data_multiple_inherit(self): s0 = [ {"F": {"@type": "text", "text": "level 0"}}, @@ -482,6 +484,7 @@ def test_get_data_multiple_inherit(self): engine = Slots(root["documents"]["internal"]) left = engine.get_data("left", full=True) + self.assertEqual( left["blocks"], { @@ -510,17 +513,11 @@ def test_get_data_multiple_inherit(self): "readOnly": True, "text": "local 2", }, - "F": { - "@type": "text", - "_v_inherit": True, - "readOnly": True, - "text": "level 0", - }, }, ) # F should not be in layout, as it's "third-hand" inherited self.assertEqual( - left["blocks_layout"], {"items": ["B", "A", "F-E-C", "D", "F"]} + left["blocks_layout"], {"items": ["B", "A", "F-E-C", "D"]} ) def test_save_slots(self): From 79eb6182949fc95793d470e477a22c988b6c3e4a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 12 Jul 2021 07:52:41 +0300 Subject: [PATCH 86/99] Fix deep inheritance test --- src/plone/restapi/slots/__init__.py | 8 ++++++-- src/plone/restapi/tests/test_slots.py | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 37b921110d..25feb90462 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -100,10 +100,12 @@ def get_data(self, name, full=False): _replaced = set() # original blocks that are overridden by variants. We # don't want to include these in the final output - __to_include_originals = [] # these blocks need to reference their original + __to_include_originals = [] # these blocks need to reference their original stack = self.get_fills_stack(name) + # TODO: I think the block data gets overridden when it's an inherited block + for level, slot in enumerate(stack): if slot is None: continue @@ -148,7 +150,9 @@ def get_data(self, name, full=False): # "unhide", we'll need the original data for best UX # TODO: what do do when the inherited block has been deleted? for v in __to_include_originals: - v["_v_original"] = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) + # original = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) + original = _seen_blocks[v.get("s:isVariantOf")] + v["_v_original"] = original return { "blocks": _blocks, diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index 1536321575..c8fa0d30f6 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -495,10 +495,24 @@ def test_get_data_multiple_inherit(self): "readOnly": True, "text": "local 1", }, + "D": { + "@type": "text", + "_v_inherit": True, + "readOnly": True, + "text": "local 2", + }, "F-E-C": { "@type": "text", "_v_original": { "@type": "text", + "_v_inherit": True, + "_v_original": { + "@type": "text", + "_v_inherit": True, + "readOnly": True, + "text": "level 0", + }, + "readOnly": True, "s:isVariantOf": "F", "text": "right customized", }, @@ -507,18 +521,10 @@ def test_get_data_multiple_inherit(self): "text": "right customized", "v:hidden": True, }, - "D": { - "@type": "text", - "_v_inherit": True, - "readOnly": True, - "text": "local 2", - }, }, ) # F should not be in layout, as it's "third-hand" inherited - self.assertEqual( - left["blocks_layout"], {"items": ["B", "A", "F-E-C", "D"]} - ) + self.assertEqual(left["blocks_layout"], {"items": ["B", "A", "F-E-C", "D"]}) def test_save_slots(self): data = { From 9eb44031eb82f7611d0eda0916a3a1a828d6cc78 Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Mon, 12 Jul 2021 11:31:34 +0300 Subject: [PATCH 87/99] Don't include _v_inherit everywhere; don't fail on deleted inherited block --- src/plone/restapi/slots/__init__.py | 27 +++++++++++++++++++-------- src/plone/restapi/tests/test_slots.py | 4 ---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 25feb90462..669e140c0b 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -98,6 +98,7 @@ def get_data(self, name, full=False): _hidden = [] # list of block uids that are hidden _seen_blocks = {} # all blocks in this hierarchy _replaced = set() # original blocks that are overridden by variants. We + _inherited = set() # block uids that are inherited # don't want to include these in the final output __to_include_originals = [] # these blocks need to reference their original @@ -125,8 +126,7 @@ def get_data(self, name, full=False): continue if level > 0: # anything deeper than "top" level is inherited - block["_v_inherit"] = True - block["readOnly"] = True + _inherited.add(uid) if uid not in _replaced: _blocks[uid] = block @@ -140,6 +140,11 @@ def get_data(self, name, full=False): break for k, v in _blocks.items(): + + if k in _inherited: + v["_v_inherit"] = True + v["readOnly"] = True + if v.get("s:sameAs"): v.update(self._resolve_block(v, _seen_blocks)) v["_v_inherit"] = True @@ -148,11 +153,14 @@ def get_data(self, name, full=False): # in the frontend, if we have a block that's hidden then we go and # "unhide", we'll need the original data for best UX - # TODO: what do do when the inherited block has been deleted? + for v in __to_include_originals: # original = deepcopy(_seen_blocks[v.get("s:isVariantOf")]) - original = _seen_blocks[v.get("s:isVariantOf")] - v["_v_original"] = original + original = _seen_blocks.get(v.get("s:isVariantOf"), None) + + # TODO: what do do when the inherited block has been deleted? + if original is not None: + v["_v_original"] = original return { "blocks": _blocks, @@ -166,10 +174,13 @@ def get_data(self, name, full=False): } def _resolve_block(self, block, blocks): - sameAs = block.get("s:sameAs") + sameAsUID = block.get("s:sameAs") - if sameAs: - return self._resolve_block(blocks[sameAs], blocks) + if sameAsUID: + if sameAsUID in blocks: + return self._resolve_block(blocks[sameAsUID], blocks) + else: + return None return block diff --git a/src/plone/restapi/tests/test_slots.py b/src/plone/restapi/tests/test_slots.py index c8fa0d30f6..9b677c0c2c 100644 --- a/src/plone/restapi/tests/test_slots.py +++ b/src/plone/restapi/tests/test_slots.py @@ -505,14 +505,10 @@ def test_get_data_multiple_inherit(self): "@type": "text", "_v_original": { "@type": "text", - "_v_inherit": True, "_v_original": { "@type": "text", - "_v_inherit": True, - "readOnly": True, "text": "level 0", }, - "readOnly": True, "s:isVariantOf": "F", "text": "right customized", }, From 66e76871283e2e33b8a4447dbae4208382bed20a Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 28 Jul 2021 19:56:19 +0300 Subject: [PATCH 88/99] WIP on new traversal for slots --- src/plone/restapi/slots/__init__.py | 12 ++++++++++++ src/plone/restapi/slots/configure.zcml | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 669e140c0b..40d1b44d82 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -47,6 +47,18 @@ def __init__(self, **data): for k, v in data.items(): setattr(self, k, v) + def getPhysicalPath(self): + """ Return physical path + + Override, to be able to provide a fake name for the physical path + """ + path = super(Slot, self).getPhysicalPath() + + res = tuple([''] + [bit for bit in path[1:] if bit]) + path = () + res[:-1] + ('++slots++' + path[-1],) + + return path + @implementer(ISlots) @adapter(ITraversable) diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index dff75fb0fa..d3312d05a6 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -42,4 +42,11 @@ name="slots" /> + + From 493f345a253c3f554ad90f5bf116d87e62e5bedb Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 28 Jul 2021 19:57:48 +0300 Subject: [PATCH 89/99] Add missing file --- src/plone/restapi/slots/traversing.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/plone/restapi/slots/traversing.py diff --git a/src/plone/restapi/slots/traversing.py b/src/plone/restapi/slots/traversing.py new file mode 100644 index 0000000000..21b1925b3b --- /dev/null +++ b/src/plone/restapi/slots/traversing.py @@ -0,0 +1,26 @@ +from plone.rest.traverse import RESTWrapper +from plone.restapi.slots.interfaces import ISlots +from zExceptions import NotFound +from zope.traversing.namespace import SimpleHandler + + +class RestSlotsTraversing(SimpleHandler): + ''' rest attachment traversing ''' + + name = None + + def __init__(self, context, request=None): + self.context = context + + def traverse(self, name, remaining): + ''' traverse ''' + + slots = ISlots(self.context.context) + + if name not in slots: + raise NotFound + + # self.context is a RESTWrapper + storage = slots.__of__(self.context.context) + + return RESTWrapper(storage[name], self.context.request) From c5723f68f94c8c432258a01b02ed5e426ecef2be Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Wed, 28 Jul 2021 22:51:46 +0300 Subject: [PATCH 90/99] WIP on new traversal for slots --- src/plone/restapi/serializer/slots.py | 6 ++-- src/plone/restapi/slots/__init__.py | 12 +++++-- src/plone/restapi/slots/configure.zcml | 7 ++++ src/plone/restapi/slots/traversing.py | 44 ++++++++++++++++++++++---- 4 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 595569ea3a..1a5f458c54 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -18,7 +18,7 @@ import copy -SERVICE_ID = "@slots" +TRAVERSER = "++slots++" _MISSING = object() @@ -71,7 +71,7 @@ def __call__(self, full=False): blocks[id] = json_compatible(block_value) result = { - "@id": "{0}/{1}/{2}".format(self.context.absolute_url(), SERVICE_ID, name), + "@id": "{0}/{1}{2}".format(self.context.absolute_url(), TRAVERSER, name), "blocks": blocks, "blocks_layout": data["blocks_layout"], } @@ -93,7 +93,7 @@ def __init__(self, context, storage, request): def __call__(self, full=False): base_url = self.context.absolute_url() - result = {"@id": "{}/{}".format(base_url, SERVICE_ID), "items": {}} + result = {"@id": "{}/{}".format(base_url, TRAVERSER), "items": {}} engine = ISlots(self.context) slot_names = engine.discover_slots() diff --git a/src/plone/restapi/slots/__init__.py b/src/plone/restapi/slots/__init__.py index 40d1b44d82..1fad9cab40 100644 --- a/src/plone/restapi/slots/__init__.py +++ b/src/plone/restapi/slots/__init__.py @@ -6,6 +6,7 @@ from AccessControl.SecurityManagement import getSecurityManager from Acquisition import Implicit from copy import deepcopy +from OFS.Traversable import Traversable from persistent import Persistent from plone.registry.interfaces import IRegistry from plone.restapi.permissions import ModifySlotsPermission @@ -35,7 +36,7 @@ class PersistentSlots(BTreeContainer): @implementer(ISlot) -class Slot(Persistent, Contained, Implicit): +class Slot(Persistent, Contained, Implicit, Traversable): """A container for data pertaining to a single slot""" def __init__(self, **data): @@ -52,11 +53,12 @@ def getPhysicalPath(self): Override, to be able to provide a fake name for the physical path """ - path = super(Slot, self).getPhysicalPath() + path = super(Slot, self).getPhysicalPath()[:-1] # last bit is RestWrapper res = tuple([''] + [bit for bit in path[1:] if bit]) path = () + res[:-1] + ('++slots++' + path[-1],) + print('path', path) return path @@ -84,6 +86,12 @@ def discover_slots(self): return names + def keys(self): + return self.discover_slots() + + def __contains__(self, name): + return name in self.keys() + def get_fills_stack(self, name): slot_stack = [] diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index d3312d05a6..7ada07a8c7 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -49,4 +49,11 @@ name="slots" /> + + diff --git a/src/plone/restapi/slots/traversing.py b/src/plone/restapi/slots/traversing.py index 21b1925b3b..e63b66b253 100644 --- a/src/plone/restapi/slots/traversing.py +++ b/src/plone/restapi/slots/traversing.py @@ -1,26 +1,56 @@ +from . import Slot +from .interfaces import ISlotStorage from plone.rest.traverse import RESTWrapper from plone.restapi.slots.interfaces import ISlots from zExceptions import NotFound from zope.traversing.namespace import SimpleHandler -class RestSlotsTraversing(SimpleHandler): - ''' rest attachment traversing ''' +def get_slot(context, name): + slots = ISlots(context) + + if name not in slots: + raise NotFound + + storage = ISlotStorage(context) + slot = storage.get(name, None) + if not slot: + slot = Slot() + + slot = slot.__of__(context) + + return slot + + +class SlotsTraversing(SimpleHandler): + ''' REST attachment traversing ''' name = None def __init__(self, context, request=None): self.context = context + self.request = request def traverse(self, name, remaining): ''' traverse ''' - slots = ISlots(self.context.context) + slot = get_slot(self.context, name) + wrapper = RESTWrapper(slot, self.request) + return wrapper - if name not in slots: - raise NotFound + +class RestSlotsTraversing(SimpleHandler): + ''' REST slots traversing ''' + + name = None + + def __init__(self, context, request=None): + self.context = context + + def traverse(self, name, remaining): + ''' traverse ''' # self.context is a RESTWrapper - storage = slots.__of__(self.context.context) + slot = get_slot(self.context.context) - return RESTWrapper(storage[name], self.context.request) + return RESTWrapper(slot, self.context.request) From 59c36449a51c967e8840d16ca5d8d7abc761c42c Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Thu, 29 Jul 2021 08:35:47 +0300 Subject: [PATCH 91/99] Adjust tests for new slots serialized ids --- src/plone/restapi/serializer/slots.py | 5 +++-- src/plone/restapi/slots/configure.zcml | 4 ++-- src/plone/restapi/tests/test_serializer_slots.py | 14 +++++++------- src/plone/restapi/tests/test_services_slots.py | 14 +++++++------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/plone/restapi/serializer/slots.py b/src/plone/restapi/serializer/slots.py index 1a5f458c54..18bfe894c0 100644 --- a/src/plone/restapi/serializer/slots.py +++ b/src/plone/restapi/serializer/slots.py @@ -18,7 +18,8 @@ import copy -TRAVERSER = "++slots++" +TRAVERSER = "++slot++" +ENDPOINT = "@slots" _MISSING = object() @@ -93,7 +94,7 @@ def __init__(self, context, storage, request): def __call__(self, full=False): base_url = self.context.absolute_url() - result = {"@id": "{}/{}".format(base_url, TRAVERSER), "items": {}} + result = {"@id": "{}/{}".format(base_url, ENDPOINT), "items": {}} engine = ISlots(self.context) slot_names = engine.discover_slots() diff --git a/src/plone/restapi/slots/configure.zcml b/src/plone/restapi/slots/configure.zcml index 7ada07a8c7..f0543c745d 100644 --- a/src/plone/restapi/slots/configure.zcml +++ b/src/plone/restapi/slots/configure.zcml @@ -46,11 +46,11 @@ factory=".traversing.RestSlotsTraversing" provides="zope.traversing.interfaces.ITraversable" for="plone.rest.traverse.RESTWrapper zope.publisher.interfaces.IRequest" - name="slots" + name="slot" />