Skip to content

Commit

Permalink
Merge branch 'master' into branch-01
Browse files Browse the repository at this point in the history
  • Loading branch information
davisagli authored Jul 12, 2023
2 parents cfc85cf + 2100a45 commit 3c564a7
Show file tree
Hide file tree
Showing 14 changed files with 134 additions and 68 deletions.
2 changes: 2 additions & 0 deletions news/1639.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Remove the hard code dependency by plone.app.multilingual, use it conditionaly instead
[@folix-01]
2 changes: 2 additions & 0 deletions news/1642.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
When serializing blocks, `image_scales` is now added to blocks that contain a resolveuid-based `url`.
When deserializing blocks, `image_scales` is removed. @davisagli
7 changes: 7 additions & 0 deletions src/plone/restapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
from Products.PluggableAuthService.PluggableAuthService import registerMultiPlugin
from zope.i18nmessageid import MessageFactory

import pkg_resources

try:
pkg_resources.get_distribution("plone.app.multilingual")
HAS_MULTILINGUAL = True
except pkg_resources.DistributionNotFound:
HAS_MULTILINGUAL = False

_ = MessageFactory("plone.restapi")
PROJECT_NAME = "plone.restapi"
Expand Down
11 changes: 3 additions & 8 deletions src/plone/restapi/deserializer/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,9 @@ def _process_data(self, data, field=None):
if data.get("@type", None) == "URL" and data.get("value", None):
data["value"] = path2uid(context=self.context, link=data["value"])
elif data.get("@id", None):
item_clone = deepcopy(data)
item_clone["@id"] = path2uid(
context=self.context, link=item_clone["@id"]
)
return {
field: self._process_data(data=value, field=field)
for field, value in item_clone.items()
}
data = deepcopy(data)
data["@id"] = path2uid(context=self.context, link=data["@id"])
data.pop("image_scales", None)
return {
field: self._process_data(data=value, field=field)
for field, value in data.items()
Expand Down
33 changes: 20 additions & 13 deletions src/plone/restapi/serializer/blocks.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from copy import deepcopy
from plone.restapi.bbb import IPloneSiteRoot
from plone.restapi.behaviors import IBlocks
from plone.restapi.blocks import visit_blocks, iter_block_transform_handlers
Expand All @@ -9,7 +8,7 @@
from plone.restapi.interfaces import IFieldSerializer
from plone.restapi.serializer.converters import json_compatible
from plone.restapi.serializer.dxfields import DefaultFieldSerializer
from plone.restapi.serializer.utils import uid_to_url
from plone.restapi.serializer.utils import resolve_uid, uid_to_url
from plone.schema import IJSONField
from zope.component import adapter
from zope.interface import implementer
Expand Down Expand Up @@ -57,19 +56,27 @@ def _process_data(self, data, field=None):
if isinstance(data, list):
return [self._process_data(data=value, field=field) for value in data]
if isinstance(data, dict):
if data.get("@type", None) == "URL" and data.get("value", None):
data["value"] = uid_to_url(data["value"])
elif data.get("@id", None):
item_clone = deepcopy(data)
item_clone["@id"] = uid_to_url(item_clone["@id"])
return {
field: self._process_data(data=value, field=field)
for field, value in item_clone.items()
}
return {
field: self._process_data(data=value, field=field)
fields = ["value"] if data.get("@type") == "URL" else []
fields.append("@id")
fields.extend(self.fields)
newdata = {}
for field in fields:
if field not in data or not isinstance(data[field], str):
continue
newdata[field], brain = resolve_uid(data[field])
if brain is not None and "image_scales" not in newdata:
newdata["image_scales"] = getattr(brain, "image_scales", None)
result = {
field: (
newdata[field]
if field in newdata
else self._process_data(data=newdata.get(field, value), field=field)
)
for field, value in data.items()
}
if newdata.get("image_scales"):
result["image_scales"] = newdata["image_scales"]
return result
return data


Expand Down
26 changes: 15 additions & 11 deletions src/plone/restapi/serializer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,25 @@
RESOLVEUID_RE = re.compile("^[./]*resolve[Uu]id/([^/]*)/?(.*)$")


def uid_to_url(path):
"""turns a resolveuid url into a real url.
def resolve_uid(path):
"""Resolves a resolveuid URL into a tuple of absolute URL and catalog brain.
This uses the catalog first, but wake up the object to check if there is
an IObjectPrimaryFieldTarget on this object. If so, it will return the
target url instead of the object url.
If the original path is not found (including external URLs),
it will be returned unchanged and the brain will be None.
"""
if not path:
return ""
return "", None
match = RESOLVEUID_RE.match(path)
if match is None:
return path
return path, None

uid, suffix = match.groups()
brain = uuidToCatalogBrain(uid)
if brain is None:
return path
return path, None
href = brain.getURL()
if suffix:
return href + "/" + suffix
return href + "/" + suffix, brain
target_object = brain._unrestrictedGetObject()
adapter = queryMultiAdapter(
(target_object, target_object.REQUEST),
Expand All @@ -39,8 +38,13 @@ def uid_to_url(path):
if adapter:
a_href = adapter()
if a_href:
return a_href
return href
return a_href, None
return href, brain


def uid_to_url(path):
path, brain = resolve_uid(path)
return path


def get_portal_type_title(portal_type):
Expand Down
7 changes: 3 additions & 4 deletions src/plone/restapi/services/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,9 @@
package=".workingcopy"
zcml:condition="installed plone.app.iterate"
/>
<include
package=".multilingual"
zcml:condition="have plone-5"
/>
<configure zcml:condition="installed plone.app.multilingual">
<include package=".multilingual" />
</configure>
<include
package=".email_notification"
zcml:condition="have plone-5"
Expand Down
10 changes: 7 additions & 3 deletions src/plone/restapi/services/content/add.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from AccessControl import getSecurityManager
from Acquisition import aq_base
from Acquisition.interfaces import IAcquirer
from plone.app.multilingual.interfaces import IPloneAppMultilingualInstalled
from plone.app.multilingual.interfaces import ITranslationManager
from plone.restapi import HAS_MULTILINGUAL
from plone.restapi.bbb import safe_hasattr
from plone.restapi.deserializer import json_body
from plone.restapi.exceptions import DeserializationError
Expand All @@ -23,6 +22,10 @@

import plone.protect.interfaces

if HAS_MULTILINGUAL:
from plone.app.multilingual.interfaces import IPloneAppMultilingualInstalled
from plone.app.multilingual.interfaces import ITranslationManager


class FolderPost(Service):
"""Creates a new content object."""
Expand Down Expand Up @@ -94,7 +97,8 @@ def reply(self):

# Link translation given the translation_of property
if (
IPloneAppMultilingualInstalled.providedBy(self.request)
HAS_MULTILINGUAL
and IPloneAppMultilingualInstalled.providedBy(self.request)
and translation_of
and language
):
Expand Down
6 changes: 0 additions & 6 deletions src/plone/restapi/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ def tearDown(self):


class PloneRestApiDXLayer(PloneSandboxLayer):

defaultBases = (DATE_TIME_FIXTURE, PLONE_APP_CONTENTTYPES_FIXTURE)

def setUpZope(self, app, configurationContext):
Expand Down Expand Up @@ -159,7 +158,6 @@ def setUpPloneSite(self, portal):


class PloneRestApiTestWorkflowsLayer(PloneSandboxLayer):

defaultBases = (PLONE_RESTAPI_DX_FIXTURE,)

def setUpPloneSite(self, portal):
Expand All @@ -174,7 +172,6 @@ def setUpPloneSite(self, portal):


class PloneRestApiDXPAMLayer(PloneSandboxLayer):

defaultBases = (DATE_TIME_FIXTURE, PLONE_APP_CONTENTTYPES_FIXTURE)

def setUpZope(self, app, configurationContext):
Expand Down Expand Up @@ -217,7 +214,6 @@ def setUpPloneSite(self, portal):
if PloneAppCachingBase is not None:
# condition and fallback can be removed in a Plone 6.0 only scenario
class PloneRestApiCachingLayer(PloneAppCachingBase):

defaultBases = [
PLONE_RESTAPI_DX_PAM_FIXTURE,
]
Expand All @@ -240,7 +236,6 @@ class PloneRestApiCachingLayer(PloneAppCachingBase):


class PloneRestApiDXIterateLayer(PloneSandboxLayer):

defaultBases = (PLONEAPPITERATEDEX_FIXTURE,)

def setUpZope(self, app, configurationContext):
Expand All @@ -264,7 +259,6 @@ def setUpZope(self, app, configurationContext):


class PloneRestApIBlocksLayer(PloneSandboxLayer):

defaultBases = (PLONE_RESTAPI_DX_FIXTURE,)

def setUpPloneSite(self, portal):
Expand Down
6 changes: 6 additions & 0 deletions src/plone/restapi/tests/test_blocks_deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,9 @@ def test_slate_table_block_deserializer(self):
cell = rows[1]["cells"][0]
link = cell["value"][0]["children"][1]["data"]["url"]
self.assertTrue(link.startswith("../resolveuid/"))

def test_deserialize_url_with_image_scales(self):
blocks = {"123": {"url": self.image.absolute_url(), "image_scales": {}}}
res = self.deserialize(blocks=blocks)
self.assertTrue(res.blocks["123"]["url"].startswith("../resolveuid/"))
self.assertNotIn("image_scales", res.blocks["123"])
25 changes: 25 additions & 0 deletions src/plone/restapi/tests/test_blocks_serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from importlib import import_module
from plone.dexterity.interfaces import IDexterityFTI
from plone.dexterity.interfaces import IDexterityItem
from plone.dexterity.utils import iterSchemata
from plone.namedfile.file import NamedBlobImage
from plone.restapi.behaviors import IBlocks
from plone.restapi.interfaces import IBlockFieldSerializationTransformer
from plone.restapi.interfaces import IFieldSerializer
Expand All @@ -15,9 +17,17 @@
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest

import pathlib
import unittest


HAS_PLONE_6 = getattr(
import_module("Products.CMFPlone.factory"), "PLONE60MARKER", False
)
IMAGE_PATH = (pathlib.Path(__file__).parent / "image.png").resolve()
IMAGE_DATA = IMAGE_PATH.read_bytes()


class TestBlocksSerializer(unittest.TestCase):

layer = PLONE_RESTAPI_DX_INTEGRATION_TESTING
Expand All @@ -36,6 +46,8 @@ def setUp(self):
self.image = self.portal[
self.portal.invokeFactory("Image", id="image-1", title="Target image")
]
self.image.image = NamedBlobImage(data=IMAGE_DATA, filename="test.jpg")
self.image.reindexObject()

def serialize(self, context, blocks):
fieldname = "blocks"
Expand Down Expand Up @@ -377,3 +389,16 @@ def test_slate_table_block_link_serializer(self):
cell = rows[1]["cells"][0]
link = cell["value"][0]["children"][1]["data"]["url"]
self.assertTrue(link, self.portal.absolute_url() + "/doc1")

@unittest.skipUnless(
HAS_PLONE_6,
"image_scales were added to the catalog in Plone 6",
)
def test_image_scales_serializer(self):
image_uid = self.image.UID()
res = self.serialize(
context=self.portal["doc1"],
blocks={"123": {"@type": "image", "url": f"../resolveuid/{image_uid}"}},
)
self.assertEqual(res["123"]["url"], self.image.absolute_url())
self.assertIn("image_scales", res["123"])
19 changes: 11 additions & 8 deletions src/plone/restapi/tests/test_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IReplies
from plone.app.multilingual.interfaces import ITranslationManager
from plone.app.testing import applyProfile
from plone.app.testing import setRoles
from plone.app.testing import SITE_OWNER_NAME
from plone.app.testing import SITE_OWNER_PASSWORD
Expand Down Expand Up @@ -35,6 +36,7 @@
from plone.app.testing import pushGlobalRegistry
from plone.restapi.testing import register_static_uuid_utility
from zope.component.hooks import getSite

import collections
import json
import os
Expand Down Expand Up @@ -213,7 +215,6 @@ def tearDown(self):


class TestDocumentation(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
Expand Down Expand Up @@ -1746,7 +1747,6 @@ def test_site_get(self):


class TestDocumentationMessageTranslations(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
Expand Down Expand Up @@ -1803,7 +1803,6 @@ def test_translate_messages_addons(self):


class TestCommenting(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
Expand Down Expand Up @@ -2072,7 +2071,6 @@ def test_aliases_root_filter(self):


class TestControlPanelDocumentation(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def test_controlpanels_get_listing(self):
Expand Down Expand Up @@ -2130,13 +2128,20 @@ def test_controlpanels_crud_dexterity(self):


class TestPAMDocumentation(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING

def setUp(self):
super().setUp()

#
language_tool = api.portal.get_tool("portal_languages")
language_tool.addSupportedLanguage("en")
language_tool.addSupportedLanguage("es")
language_tool.addSupportedLanguage("de")
if api.portal.get().portal_setup.profileExists(
"plone.app.multilingual:default"
):
applyProfile(self.portal, "plone.app.multilingual:default")

# We manually set the UIDs for LRFs here because the static uuid
# generator is not applied for LRFs.
# When we have tried to apply it for LRFs we have had several
Expand Down Expand Up @@ -2248,7 +2253,6 @@ def test_site_expansion_navroot_language_folder_content(self):


class TestIterateDocumentation(TestDocumentationBase):

layer = PLONE_RESTAPI_ITERATE_FUNCTIONAL_TESTING

def setUp(self):
Expand Down Expand Up @@ -2352,7 +2356,6 @@ def test_documentation_schema_user(self):


class TestRules(TestDocumentationBase):

layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING

def setUp(self):
Expand Down
Loading

0 comments on commit 3c564a7

Please sign in to comment.