Skip to content

Commit

Permalink
Merge branch 'main' into erral-login-options
Browse files Browse the repository at this point in the history
  • Loading branch information
erral authored Jan 10, 2025
2 parents 730e8d2 + 7214df3 commit 3468580
Show file tree
Hide file tree
Showing 17 changed files with 420 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ on: [push, pull_request]
jobs:
build:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
Expand Down
52 changes: 52 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,58 @@ Changelog
.. towncrier release notes start
9.9.0 (2024-12-18)
------------------

New features:


- When a Link content item is linked by UID, resolve its URL as the linked target URL for anonymous users. @cekk (#1847)


Bug fixes:


- Fix resolving paths in deserializer if the target was moved in the same request. @cekk (#1848)
- Make slate block linkintegrity checking more robust in case data isn't in the expected format. @cekk (#1849)
- Optimized performance of DexterityObjectPrimaryFieldTarget adapter. @maurits (#1851)


Internal:


- Fix time-dependence of tests. @davisagli (#1850)


9.8.5 (2024-11-25)
------------------

Bug fixes:


- Fix log in after changing email when "email as login" is enabled
[erral] (#1835)
- Fix tests after #1839 and plone.app.event#411
[erral] (#1844)
- Do not change request during relation fields serialization
[cekk] (#1845)


Internal:


- Test that recurrence serialization provides correct data
[erral] (#1809)
- Additional tests to login name changes
[erral] (#1840)


Documentation:


- `html_use_opensearch` value must not have a trailing slash. Clean up comments. @stevepiercy (#1846)


9.8.4 (2024-11-05)
------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def patch_pygments_to_highlight_jsonschema():
# base URL from which the finished HTML is served.
# Announce that we have an opensearch plugin
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_use_opensearch
html_use_opensearch = "https://plonerestapi.readthedocs.org/"
html_use_opensearch = "https://plonerestapi.readthedocs.org"


# This is the file name suffix for HTML files (e.g. ".xhtml").
Expand Down
2 changes: 0 additions & 2 deletions news/1835.bugfix

This file was deleted.

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import sys


version = "9.8.5.dev0"
version = "9.9.1.dev0"

if sys.version_info.major == 2:
raise ValueError(
Expand Down
3 changes: 1 addition & 2 deletions src/plone/restapi/blocks_linkintegrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ def __init__(self, context, request):
def __call__(self, block):
value = (block or {}).get(self.field, [])
children = iterate_children(value or [])

for child in children:
node_type = child.get("type")
node_type = child.get("type", "")
if node_type:
handler = getattr(self, f"handle_{node_type}", None)
if handler:
Expand Down
7 changes: 4 additions & 3 deletions src/plone/restapi/deserializer/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ def iterate_children(value):
queue = deque(value)
while queue:
child = queue.pop()
yield child
if child.get("children"):
queue.extend(child["children"] or [])
if isinstance(child, dict):
yield child
if child.get("children", []):
queue.extend(child["children"] or [])


@implementer(IFieldDeserializer)
Expand Down
11 changes: 11 additions & 0 deletions src/plone/restapi/deserializer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
from plone.uuid.interfaces import IUUID
from plone.uuid.interfaces import IUUIDAware
from zope.component import getMultiAdapter
from plone.app.redirector.interfaces import IRedirectionStorage
from zope.component import getUtility

import re

PATH_RE = re.compile(r"^(.*?)((?=/@@|#).*)?$")
Expand Down Expand Up @@ -35,6 +38,14 @@ def path2uid(context, link):
suffix = match.group(2) or ""

obj = portal.unrestrictedTraverse(path, None)
if obj is None:
# last try: maybe the object or some parent has been renamed.
# if yes, there should be a reference into redirection storage
storage = getUtility(IRedirectionStorage)
alias_path = storage.get(path)
if alias_path:
path = alias_path
obj = portal.unrestrictedTraverse(path, None)
if obj is None or obj == portal:
return link
segments = path.split("/")
Expand Down
2 changes: 2 additions & 0 deletions src/plone/restapi/serializer/configure.zcml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<adapter factory=".dxcontent.SerializeToJson" />
<adapter factory=".dxcontent.SerializeFolderToJson" />
<adapter factory=".dxcontent.DexterityObjectPrimaryFieldTarget" />
<adapter factory=".dxcontent.LinkObjectPrimaryFieldTarget" />


<configure zcml:condition="installed plone.app.contenttypes">
<adapter factory=".collection.SerializeCollectionToJson" />
Expand Down
56 changes: 42 additions & 14 deletions src/plone/restapi/serializer/dxcontent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from AccessControl import getSecurityManager
from Acquisition import aq_inner
from Acquisition import aq_parent
from plone.app.contenttypes.interfaces import ILink
from plone.autoform.interfaces import READ_PERMISSIONS_KEY
from plone.dexterity.interfaces import IDexterityContainer
from plone.dexterity.interfaces import IDexterityContent
Expand Down Expand Up @@ -219,23 +220,26 @@ def __init__(self, context, request):

def __call__(self):
primary_field_name = self.get_primary_field_name()
if not primary_field_name:
return
for schema in iterSchemata(self.context):
read_permissions = mergedTaggedValueDict(schema, READ_PERMISSIONS_KEY)

for name, field in getFields(schema).items():
if not self.check_permission(read_permissions.get(name), self.context):
continue

if name != primary_field_name:
continue

target_adapter = queryMultiAdapter(
(field, self.context, self.request), IPrimaryFieldTarget
)
if target_adapter:
target = target_adapter()
if target:
return target
field = getFields(schema).get(primary_field_name)
if field is None:
continue
if not self.check_permission(
read_permissions.get(primary_field_name),
self.context,
):
return

target_adapter = queryMultiAdapter(
(field, self.context, self.request), IPrimaryFieldTarget
)
if not target_adapter:
return
return target_adapter()

def get_primary_field_name(self):
fieldname = None
Expand Down Expand Up @@ -266,3 +270,27 @@ def check_permission(self, permission_name, obj):
sm.checkPermission(permission.title, obj)
)
return self.permission_cache[permission_name]


@adapter(ILink, Interface)
@implementer(IObjectPrimaryFieldTarget)
class LinkObjectPrimaryFieldTarget:
def __init__(self, context, request):
self.context = context
self.request = request

self.permission_cache = {}

def __call__(self):
"""
If user can edit Link object, do not return remoteUrl
"""
pm = getToolByName(self.context, "portal_membership")
if bool(pm.isAnonymousUser()):
for schema in iterSchemata(self.context):
for name, field in getFields(schema).items():
if name == "remoteUrl":
serializer = queryMultiAdapter(
(field, self.context, self.request), IFieldSerializer
)
return serializer()
2 changes: 1 addition & 1 deletion src/plone/restapi/serializer/relationfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@implementer(IJsonCompatible)
def relationvalue_converter(value):
if value.to_object:
request = getRequest()
request = getRequest().clone()
request.form["metadata_fields"] = ["UID"]
summary = getMultiAdapter((value.to_object, request), ISerializeToJsonSummary)()
return json_compatible(summary)
Expand Down
27 changes: 25 additions & 2 deletions src/plone/restapi/services/users/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,20 @@ def reply(self):
if security.use_email_as_login and "email" in user_settings_to_update:
value = user_settings_to_update["email"]
pas = getToolByName(self.context, "acl_users")
pas.updateLoginName(user.getId(), value)

try:
pas.updateLoginName(user.getId(), value)
except ValueError:
return self._error(
400,
"Bad request",
_(
"Cannot update login name of user to '${new_email}'.",
mapping={
"new_email": value,
},
),
)

roles = user_settings_to_update.get("roles", {})
if roles:
Expand Down Expand Up @@ -149,7 +162,17 @@ def reply(self):

if security.use_email_as_login and "email" in user_settings_to_update:
value = user_settings_to_update["email"]
set_own_login_name(user, value)
try:
set_own_login_name(user, value)
except ValueError:
return self._error(
400,
"Bad request",
_(
"Cannot update login name of user to '${new_email}'.",
mapping={"new_email": value},
),
)

else:
if self._is_anonymous:
Expand Down
14 changes: 14 additions & 0 deletions src/plone/restapi/tests/test_blocks_deserializer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from plone import api
from plone.dexterity.interfaces import IDexterityFTI
from plone.dexterity.interfaces import IDexterityItem
from plone.restapi.behaviors import IBlocks
Expand Down Expand Up @@ -724,3 +725,16 @@ def test_deserialize_url_with_image_scales(self):
res = self.deserialize(blocks=blocks)
self.assertTrue(res.blocks["123"]["url"].startswith("../resolveuid/"))
self.assertNotIn("image_scales", res.blocks["123"])

def test_deserializer_resolve_path_also_if_it_is_an_alias(self):

self.portal.invokeFactory(
"Document",
id="doc",
)
api.content.move(source=self.portal.doc, id="renamed-doc")
blocks = {"abc": {"href": "%s/doc" % self.portal.absolute_url()}}

res = self.deserialize(blocks=blocks)
link = res.blocks["abc"]["href"]
self.assertEqual(link, f"../resolveuid/{self.portal['renamed-doc'].UID()}")
37 changes: 37 additions & 0 deletions src/plone/restapi/tests/test_dxcontent_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
from plone.namedfile.file import NamedFile
from plone.registry.interfaces import IRegistry
from plone.restapi.interfaces import IExpandableElement
from plone.restapi.interfaces import IObjectPrimaryFieldTarget
from plone.restapi.interfaces import ISerializeToJson
from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING
from plone.restapi.tests.test_expansion import ExpandableElementFoo
from plone.restapi.serializer.utils import get_portal_type_title
from plone.uuid.interfaces import IMutableUUID
from plone.uuid.interfaces import IUUID
from Products.CMFCore.utils import getToolByName
from zope.component import getGlobalSiteManager
from zope.component import getMultiAdapter
Expand Down Expand Up @@ -756,3 +758,38 @@ def test_primary_field_target_with_edit_permissions(self):
serializer = getMultiAdapter((self.portal.doc1, self.request), ISerializeToJson)
data = serializer()
self.assertNotIn("targetUrl", data)

def test_primary_field_target_for_link_objects_for_auth_return_none(self):
self.portal.invokeFactory(
"Document",
id="linked",
)
self.portal.invokeFactory(
"Link",
id="link",
remoteUrl=f"../resolveuid/{IUUID(self.portal.linked)}",
)
wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(self.portal.linked, "publish")
adapter = getMultiAdapter(
(self.portal.link, self.request), IObjectPrimaryFieldTarget
)
self.assertEqual(adapter(), None)

def test_primary_field_target_for_link_objects_for_anonymous(self):
self.portal.invokeFactory(
"Document",
id="linked",
)
self.portal.invokeFactory(
"Link",
id="link",
remoteUrl=f"../resolveuid/{IUUID(self.portal.linked)}",
)
wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(self.portal.linked, "publish")
logout()
adapter = getMultiAdapter(
(self.portal.link, self.request), IObjectPrimaryFieldTarget
)
self.assertEqual(adapter(), self.portal.linked.absolute_url())
13 changes: 13 additions & 0 deletions src/plone/restapi/tests/test_dxfield_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,19 @@ def test_relationlist_field_serialization_returns_list(self):
value,
)

def test_relation_field_serialization_do_not_change_request(self):
self.request.form["metadata_fields"] = ["foo", "bar"]
doc2 = self.portal[
self.portal.invokeFactory(
"DXTestDocument",
id="doc2",
title="Referenceable Document",
description="Description 2",
)
]
self.serialize("test_relationchoice_field", doc2)
self.assertEqual(self.request.form["metadata_fields"], ["foo", "bar"])

def test_remoteurl_field_in_links_get_converted(self):
link = self.portal[
self.portal.invokeFactory(
Expand Down
Loading

0 comments on commit 3468580

Please sign in to comment.