From aeb3ad0ccb550860ed32398a0632b14c0c61561b Mon Sep 17 00:00:00 2001 From: Dylan Jay Date: Wed, 15 Jan 2025 05:53:17 +0700 Subject: [PATCH 01/17] Handle bad input as BadRequest instead of ValueError (#1855) * Handle bad input as BadRequest instead of ValueError useful so it can be more easily ignored * added basic test * Update src/plone/restapi/services/navigation/get.py Co-authored-by: David Glick * batching int valueerror to badrequests * fix flake8 * add changelog * Update news/1855.bugfix --------- Co-authored-by: David Glick --- news/1855.bugfix | 1 + src/plone/restapi/batching.py | 18 +++++++++++------- src/plone/restapi/services/navigation/get.py | 6 +++++- src/plone/restapi/tests/test_batching.py | 5 +++++ .../restapi/tests/test_services_navigation.py | 8 ++++++++ 5 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 news/1855.bugfix diff --git a/news/1855.bugfix b/news/1855.bugfix new file mode 100644 index 0000000000..b164e9f037 --- /dev/null +++ b/news/1855.bugfix @@ -0,0 +1 @@ +Changed bad int inputs into 500 Exceptions to 400 BadRequest so they can be filtered out of logs more easily. @djay \ No newline at end of file diff --git a/src/plone/restapi/batching.py b/src/plone/restapi/batching.py index 7b0b814eea..30653c43c3 100644 --- a/src/plone/restapi/batching.py +++ b/src/plone/restapi/batching.py @@ -1,7 +1,9 @@ from plone.batching.batch import Batch from plone.restapi.deserializer import json_body +from plone.restapi.exceptions import DeserializationError from urllib.parse import parse_qsl from urllib.parse import urlencode +from zExceptions import BadRequest DEFAULT_BATCH_SIZE = 25 @@ -11,13 +13,15 @@ class HypermediaBatch: def __init__(self, request, results): self.request = request - self.b_start = int(json_body(self.request).get("b_start", False)) or int( - self.request.form.get("b_start", 0) - ) - self.b_size = int(json_body(self.request).get("b_size", False)) or int( - self.request.form.get("b_size", DEFAULT_BATCH_SIZE) - ) - + try: + self.b_start = int(json_body(self.request).get("b_start", False)) or int( + self.request.form.get("b_start", 0) + ) + self.b_size = int(json_body(self.request).get("b_size", False)) or int( + self.request.form.get("b_size", DEFAULT_BATCH_SIZE) + ) + except (ValueError, DeserializationError) as e: + raise BadRequest(e) self.batch = Batch(results, self.b_size, self.b_start) def __iter__(self): diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py index a21a52abe4..de9f91ed6b 100644 --- a/src/plone/restapi/services/navigation/get.py +++ b/src/plone/restapi/services/navigation/get.py @@ -17,6 +17,7 @@ from zope.i18n import translate from zope.interface import implementer from zope.interface import Interface +from zExceptions import BadRequest @implementer(IExpandableElement) @@ -29,7 +30,10 @@ def __init__(self, context, request): def __call__(self, expand=False): if self.request.form.get("expand.navigation.depth", False): - self.depth = int(self.request.form["expand.navigation.depth"]) + try: + self.depth = int(self.request.form["expand.navigation.depth"]) + except (ValueError, TypeError) as e: + raise BadRequest(e) else: self.depth = 1 diff --git a/src/plone/restapi/tests/test_batching.py b/src/plone/restapi/tests/test_batching.py index 3e809ebabc..4470b2c8f6 100644 --- a/src/plone/restapi/tests/test_batching.py +++ b/src/plone/restapi/tests/test_batching.py @@ -185,6 +185,11 @@ def test_batching_links_omitted_if_resulset_fits_in_single_batch(self): response = self.api_session.get("/collection?b_size=100") self.assertNotIn("batching", list(response.json())) + def test_batching_badrequests(self): + response = self.api_session.get("/collection?b_size=php") + self.assertEqual(response.status_code, 400) + self.assertIn("invalid literal for int()", response.json()["message"]) + class TestBatchingDXFolders(TestBatchingDXBase): diff --git a/src/plone/restapi/tests/test_services_navigation.py b/src/plone/restapi/tests/test_services_navigation.py index 890c5c1220..a61009005c 100644 --- a/src/plone/restapi/tests/test_services_navigation.py +++ b/src/plone/restapi/tests/test_services_navigation.py @@ -239,3 +239,11 @@ def test_use_nav_title_when_available_and_set(self): ) self.assertEqual(response.json()["items"][1]["items"][-1]["title"], nav_title) + + def test_navigation_badrequests(self): + response = self.api_session.get( + "/folder/@navigation", params={"expand.navigation.depth": "php"} + ) + + self.assertEqual(response.status_code, 400) + self.assertIn("invalid literal for int()", response.json()["message"]) From 7af964916aa59cb38b33700198829b8372c1b4ea Mon Sep 17 00:00:00 2001 From: Dylan Jay Date: Wed, 15 Jan 2025 08:48:13 +0700 Subject: [PATCH 02/17] Handle more BadRequests (#1857) * Another should be BadRequest found in the wild * add changelog * Update news/1857.bugfix --------- Co-authored-by: David Glick --- news/1857.bugfix | 1 + src/plone/restapi/services/querystringsearch/get.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 news/1857.bugfix diff --git a/news/1857.bugfix b/news/1857.bugfix new file mode 100644 index 0000000000..ea4ba7b398 --- /dev/null +++ b/news/1857.bugfix @@ -0,0 +1 @@ +Handle TypeError on querystringsearch as BadRequest. @djay \ No newline at end of file diff --git a/src/plone/restapi/services/querystringsearch/get.py b/src/plone/restapi/services/querystringsearch/get.py index acd9f3647b..9b3c0e60c7 100644 --- a/src/plone/restapi/services/querystringsearch/get.py +++ b/src/plone/restapi/services/querystringsearch/get.py @@ -33,17 +33,17 @@ def __call__(self): query = data.get("query", None) try: b_start = int(data.get("b_start", 0)) - except ValueError: + except (ValueError, TypeError): raise BadRequest("Invalid b_start") try: b_size = int(data.get("b_size", 25)) - except ValueError: + except (ValueError, TypeError): raise BadRequest("Invalid b_size") sort_on = data.get("sort_on", None) sort_order = data.get("sort_order", None) try: limit = int(data.get("limit", 1000)) - except ValueError: + except (ValueError, TypeError): raise BadRequest("Invalid limit") fullobjects = bool(data.get("fullobjects", False)) From 74f3d72dfbbfb9254821630620c0d12320eefa9e Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Fri, 17 Jan 2025 23:49:28 +0100 Subject: [PATCH 03/17] add new @login endpoint to return available external login options (#1757) * add new @login endpoint to return available external login options * changelog * lint * lint * lint * lint * lint * Update news/1757.feature Co-authored-by: Steve Piercy * Update news/1757.feature Co-authored-by: Steve Piercy * Update news/1757.feature Co-authored-by: Steve Piercy * add docs * yaml * yaml * docs * docs * Review of docs * Revert `'` to `"` * properly implement the adapter in tests * add docs rsults * black * fix response * rename the interface to ILoginProviders * Apply suggestions from code review --------- Co-authored-by: Steve Piercy Co-authored-by: David Glick --- docs/source/endpoints/index.md | 9 +-- docs/source/endpoints/login.md | 71 +++++++++++++++++++ news/1757.feature | 1 + src/plone/restapi/interfaces.py | 11 +++ .../restapi/services/auth/configure.zcml | 7 ++ src/plone/restapi/services/auth/get.py | 14 ++++ .../external_authentication_links.req | 3 + .../external_authentication_links.resp | 19 +++++ src/plone/restapi/tests/test_auth.py | 57 +++++++++++++++ src/plone/restapi/tests/test_documentation.py | 38 ++++++++++ 10 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 docs/source/endpoints/login.md create mode 100644 news/1757.feature create mode 100644 src/plone/restapi/services/auth/get.py create mode 100644 src/plone/restapi/tests/http-examples/external_authentication_links.req create mode 100644 src/plone/restapi/tests/http-examples/external_authentication_links.resp diff --git a/docs/source/endpoints/index.md b/docs/source/endpoints/index.md index 800b9f5286..2eb88c4de5 100644 --- a/docs/source/endpoints/index.md +++ b/docs/source/endpoints/index.md @@ -1,10 +1,10 @@ --- myst: html_meta: - "description": "Usage of the Plone REST API." - "property=og:description": "Usage of the Plone REST API." - "property=og:title": "Usage of the Plone REST API" - "keywords": "Plone, plone.restapi, REST, API, Usage" + "description": "Endpoints of the Plone REST API." + "property=og:description": "Endpoints of the Plone REST API." + "property=og:title": "Endpoints of the Plone REST API" + "keywords": "Plone, plone.restapi, REST, API, endpoints" --- (restapi-endpoints)= @@ -33,6 +33,7 @@ groups history linkintegrity locking +login navigation navroot actions diff --git a/docs/source/endpoints/login.md b/docs/source/endpoints/login.md new file mode 100644 index 0000000000..8541bb91be --- /dev/null +++ b/docs/source/endpoints/login.md @@ -0,0 +1,71 @@ +--- +myst: + html_meta: + "description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." + "property=og:description": "The @login endpoint exposes the list of external authentication services that may be used in the Plone site." + "property=og:title": "@login for external authentication links" + "keywords": "Plone, plone.restapi, REST, API, login, authentication, external services" +--- + +# Login for external authentication links + +It is common to use add-ons that allow logging in to your site using third party services. +Such add-ons include using authentication services provided by KeyCloak, GitHub, or other OAuth2 or OpenID Connect enabled services. + +When you install one of these add-ons, it modifies the login process, directing the user to third party services. + +To expose the links provided by these add-ons, `plone.restapi` provides an adapter based service registration. +It lets those add-ons know that the REST API can use those services to authenticate users. +This will mostly be used by frontends that need to show the end user the links to those services. + +To achieve that, third party products need to register one or more adapters for the Plone site root object, providing the `plone.restapi.interfaces.IExternalLoginProviders` interface. + +In the adapter, the add-on needs to return the list of external links and some metadata, including the `id`, `title`, and name of the `plugin`. + +An example adapter would be the following, in a file named {file}`adapter.py`: + +```python +from zope.component import adapter +from zope.interface import implementer + +@adapter(IPloneSiteRoot) +@implementer(IExternalLoginProviders) +class MyExternalLinks: + def __init__(self, context): + self.context = context + + def get_providers(self): + return [ + { + "id": "myprovider", + "title": "Provider", + "plugin": "pas.plugins.authomatic", + "url": "https://some.example.com/login-url", + }, + { + "id": "github", + "title": "GitHub", + "plugin": "pas.plugins.authomatic", + "url": "https://some.example.com/login-authomatic/github", + }, + ] +``` + +With the corresponding ZCML stanza, in the corresponding {file}`configure.zcml` file: + +```xml + +``` + +The API request would be as follows: + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/external_authentication_links.req +``` + +The server will respond with a `Status 200` and the list of external providers: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/external_authentication_links.resp +:language: http +``` diff --git a/news/1757.feature b/news/1757.feature new file mode 100644 index 0000000000..c678441b4e --- /dev/null +++ b/news/1757.feature @@ -0,0 +1 @@ +Add a `@login` endpoint to get external login services' links. @erral diff --git a/src/plone/restapi/interfaces.py b/src/plone/restapi/interfaces.py index 5c2aa337e6..9d5c2bcede 100644 --- a/src/plone/restapi/interfaces.py +++ b/src/plone/restapi/interfaces.py @@ -240,3 +240,14 @@ class IBlockVisitor(Interface): def __call__(self, block): """Return an iterable of sub-blocks found inside `block`.""" + + +class ILoginProviders(Interface): + """An interface needed to be implemented by providers that want to be listed + in the @login endpoint + """ + + def get_providers(): + """ + return a list of login providers, with its id, title, plugin and url + """ diff --git a/src/plone/restapi/services/auth/configure.zcml b/src/plone/restapi/services/auth/configure.zcml index dec5304c50..f5604d81f2 100644 --- a/src/plone/restapi/services/auth/configure.zcml +++ b/src/plone/restapi/services/auth/configure.zcml @@ -3,6 +3,13 @@ xmlns:plone="http://namespaces.plone.org/plone" xmlns:zcml="http://namespaces.zope.org/zcml" > + Date: Tue, 21 Jan 2025 12:30:22 +0700 Subject: [PATCH 04/17] Add parse_int to handle all cases of BadRequest from ints passed in (#1858) * Another should be BadRequest found in the wild * add changelog * add parse_int util * use parse_int for navigation also * improve bad int message * change log * fix flake8/black * use parse_int for comment_ids * fix black * Update news/1858.bugfix --------- Co-authored-by: David Glick --- news/1858.bugfix | 1 + src/plone/restapi/batching.py | 16 +++++++++------- src/plone/restapi/deserializer/__init__.py | 17 +++++++++++++++++ .../restapi/services/discussion/conversation.py | 10 +++++----- src/plone/restapi/services/navigation/get.py | 10 ++-------- .../restapi/services/querystringsearch/get.py | 17 +++++------------ src/plone/restapi/tests/test_batching.py | 6 +++++- .../restapi/tests/test_services_navigation.py | 2 +- 8 files changed, 45 insertions(+), 34 deletions(-) create mode 100644 news/1858.bugfix diff --git a/news/1858.bugfix b/news/1858.bugfix new file mode 100644 index 0000000000..b275567243 --- /dev/null +++ b/news/1858.bugfix @@ -0,0 +1 @@ +Add parse_int to handle all cases of BadRequests from ints passed in. @djay \ No newline at end of file diff --git a/src/plone/restapi/batching.py b/src/plone/restapi/batching.py index 30653c43c3..7d9bec01ba 100644 --- a/src/plone/restapi/batching.py +++ b/src/plone/restapi/batching.py @@ -1,5 +1,6 @@ from plone.batching.batch import Batch from plone.restapi.deserializer import json_body +from plone.restapi.deserializer import parse_int from plone.restapi.exceptions import DeserializationError from urllib.parse import parse_qsl from urllib.parse import urlencode @@ -14,14 +15,15 @@ def __init__(self, request, results): self.request = request try: - self.b_start = int(json_body(self.request).get("b_start", False)) or int( - self.request.form.get("b_start", 0) - ) - self.b_size = int(json_body(self.request).get("b_size", False)) or int( - self.request.form.get("b_size", DEFAULT_BATCH_SIZE) - ) - except (ValueError, DeserializationError) as e: + data = json_body(request) + except DeserializationError as e: raise BadRequest(e) + self.b_start = parse_int(data, "b_start", False) or parse_int( + self.request.form, "b_start", 0 + ) + self.b_size = parse_int(data, "b_size", False) or parse_int( + self.request.form, "b_size", DEFAULT_BATCH_SIZE + ) self.batch = Batch(results, self.b_size, self.b_start) def __iter__(self): diff --git a/src/plone/restapi/deserializer/__init__.py b/src/plone/restapi/deserializer/__init__.py index a5112dae93..0096235c67 100644 --- a/src/plone/restapi/deserializer/__init__.py +++ b/src/plone/restapi/deserializer/__init__.py @@ -1,4 +1,5 @@ from plone.restapi.exceptions import DeserializationError +from zExceptions import BadRequest import json @@ -28,3 +29,19 @@ def boolean_value(value): """ return value not in {False, "false", "False", "0", 0} + + +def parse_int(data, prop, default): + """ + Args: + data: dict from a request + prop: name of a integer paramater in the dict + default: default if not found + + Returns: an integer + Raises: BadRequest if not an int + """ + try: + return int(data.get(prop, default)) + except (ValueError, TypeError): + raise BadRequest(f"Invalid {prop}: Not an integer") diff --git a/src/plone/restapi/services/discussion/conversation.py b/src/plone/restapi/services/discussion/conversation.py index 071d75b04d..36b0b5b860 100644 --- a/src/plone/restapi/services/discussion/conversation.py +++ b/src/plone/restapi/services/discussion/conversation.py @@ -2,7 +2,7 @@ from plone.app.discussion.browser.comment import EditCommentForm from plone.app.discussion.browser.comments import CommentForm from plone.app.discussion.interfaces import IConversation -from plone.restapi.deserializer import json_body +from plone.restapi.deserializer import json_body, parse_int from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service from plone.restapi.services.discussion.utils import can_delete @@ -38,7 +38,7 @@ class CommentsGet(Service): def publishTraverse(self, request, name): if name: - self.comment_id = int(name) + self.comment_id = parse_int(dict(comment_id=name), "comment_id", None) return self def reply(self): @@ -57,7 +57,7 @@ class CommentsAdd(Service): def publishTraverse(self, request, name): if name: - self.comment_id = int(name) + self.comment_id = parse_int(dict(comment_id=name), "comment_id", None) request["form.widgets.in_reply_to"] = name return self @@ -96,7 +96,7 @@ class CommentsUpdate(Service): def publishTraverse(self, request, name): if name: - self.comment_id = int(name) + self.comment_id = parse_int(dict(comment_id=name), "comment_id", None) request["form.widgets.comment_id"] = name return self @@ -140,7 +140,7 @@ class CommentsDelete(Service): comment_id = None def publishTraverse(self, request, name): - self.comment_id = int(name) + self.comment_id = parse_int(dict(comment_id=name), "comment_id", None) return self def reply(self): diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py index de9f91ed6b..96748f336f 100644 --- a/src/plone/restapi/services/navigation/get.py +++ b/src/plone/restapi/services/navigation/get.py @@ -6,6 +6,7 @@ from plone.registry.interfaces import IRegistry from plone.restapi.bbb import INavigationSchema from plone.restapi.bbb import safe_text +from plone.restapi.deserializer import parse_int from plone.restapi.interfaces import IExpandableElement from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service @@ -17,7 +18,6 @@ from zope.i18n import translate from zope.interface import implementer from zope.interface import Interface -from zExceptions import BadRequest @implementer(IExpandableElement) @@ -29,13 +29,7 @@ def __init__(self, context, request): self.portal = getSite() def __call__(self, expand=False): - if self.request.form.get("expand.navigation.depth", False): - try: - self.depth = int(self.request.form["expand.navigation.depth"]) - except (ValueError, TypeError) as e: - raise BadRequest(e) - else: - self.depth = 1 + self.depth = parse_int(self.request.form, "expand.navigation.depth", 1) result = {"navigation": {"@id": f"{self.context.absolute_url()}/@navigation"}} if not expand: diff --git a/src/plone/restapi/services/querystringsearch/get.py b/src/plone/restapi/services/querystringsearch/get.py index 9b3c0e60c7..5e2bfee45d 100644 --- a/src/plone/restapi/services/querystringsearch/get.py +++ b/src/plone/restapi/services/querystringsearch/get.py @@ -2,6 +2,7 @@ from pkg_resources import parse_version from plone.restapi.bbb import IPloneSiteRoot from plone.restapi.deserializer import json_body +from plone.restapi.deserializer import parse_int from plone.restapi.exceptions import DeserializationError from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service @@ -31,20 +32,12 @@ def __call__(self): raise BadRequest(str(err)) query = data.get("query", None) - try: - b_start = int(data.get("b_start", 0)) - except (ValueError, TypeError): - raise BadRequest("Invalid b_start") - try: - b_size = int(data.get("b_size", 25)) - except (ValueError, TypeError): - raise BadRequest("Invalid b_size") + + b_start = parse_int(data, "b_start", 0) + b_size = parse_int(data, "b_size", 25) sort_on = data.get("sort_on", None) sort_order = data.get("sort_order", None) - try: - limit = int(data.get("limit", 1000)) - except (ValueError, TypeError): - raise BadRequest("Invalid limit") + limit = parse_int(data, "limit", 1000) fullobjects = bool(data.get("fullobjects", False)) if not query: diff --git a/src/plone/restapi/tests/test_batching.py b/src/plone/restapi/tests/test_batching.py index 4470b2c8f6..a1b4414e38 100644 --- a/src/plone/restapi/tests/test_batching.py +++ b/src/plone/restapi/tests/test_batching.py @@ -188,7 +188,11 @@ def test_batching_links_omitted_if_resulset_fits_in_single_batch(self): def test_batching_badrequests(self): response = self.api_session.get("/collection?b_size=php") self.assertEqual(response.status_code, 400) - self.assertIn("invalid literal for int()", response.json()["message"]) + self.assertIn("Invalid b_size", response.json()["message"]) + + response = self.api_session.get("/collection?b_size:list=1") + self.assertEqual(response.status_code, 400) + self.assertIn("Invalid b_size", response.json()["message"]) class TestBatchingDXFolders(TestBatchingDXBase): diff --git a/src/plone/restapi/tests/test_services_navigation.py b/src/plone/restapi/tests/test_services_navigation.py index a61009005c..c0bc6149f4 100644 --- a/src/plone/restapi/tests/test_services_navigation.py +++ b/src/plone/restapi/tests/test_services_navigation.py @@ -246,4 +246,4 @@ def test_navigation_badrequests(self): ) self.assertEqual(response.status_code, 400) - self.assertIn("invalid literal for int()", response.json()["message"]) + self.assertIn("Invalid expand.navigation.depth", response.json()["message"]) From b8f1594a968fb33c78205980e4414d1c4b429e2f Mon Sep 17 00:00:00 2001 From: David Glick Date: Tue, 21 Jan 2025 18:54:28 -0800 Subject: [PATCH 05/17] Update Plone test versions, and pin twine (#1865) * Update Plone test versions, and pin twine * Avoid overriding version pins * Remove Python 3.8 from the test matrix for Plone 6.0, where it is no longer supported * update test output for new minor Plone version --- .github/workflows/tests.yml | 2 -- news/1685.internal | 1 + plone-6.0.x-python3.8.cfg | 3 ++- plone-6.0.x.cfg | 17 ++--------------- plone-6.1.x.cfg | 9 +-------- requirements-6.0.txt | 2 +- requirements-6.1.txt | 2 +- .../tests/http-examples/registry_get_list.resp | 2 +- .../restapi/tests/http-examples/site_get.resp | 2 +- 9 files changed, 10 insertions(+), 30 deletions(-) create mode 100644 news/1685.internal diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 72d7ada3ff..bd31daf00a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,8 +10,6 @@ jobs: include: - python-version: "3.8" plone-version: "5.2" - - python-version: "3.8" - plone-version: "6.0" - python-version: "3.9" plone-version: "6.0" - python-version: "3.10" diff --git a/news/1685.internal b/news/1685.internal new file mode 100644 index 0000000000..6fe9f0e0dc --- /dev/null +++ b/news/1685.internal @@ -0,0 +1 @@ +Update CI. @davisagli diff --git a/plone-6.0.x-python3.8.cfg b/plone-6.0.x-python3.8.cfg index 2bd12daf3f..c093e32964 100644 --- a/plone-6.0.x-python3.8.cfg +++ b/plone-6.0.x-python3.8.cfg @@ -1,6 +1,6 @@ [buildout] extends = - https://dist.plone.org/release/6.0.12/versions.cfg + https://dist.plone.org/release/6.0.14/versions.cfg base.cfg [instance] @@ -15,3 +15,4 @@ robotframework-assertion-engine = 2.0.0 robotframework-debuglibrary = 2.3.0 robotframework-pythonlibcore = 4.2.0 grpcio-tools = 1.59.0 +twine = 5.1.1 diff --git a/plone-6.0.x.cfg b/plone-6.0.x.cfg index 26373fac81..eba5ea4141 100644 --- a/plone-6.0.x.cfg +++ b/plone-6.0.x.cfg @@ -1,24 +1,11 @@ [buildout] extends = - https://dist.plone.org/release/6.0.12/versions.cfg + https://dist.plone.org/release/6.0.14/versions.cfg base.cfg -[buildout:python37] -parts = - test - code-analysis - [instance] recipe = plone.recipe.zope2instance zodb-temporary-storage = off [versions] -# Override pin from Zope. https://github.com/zopefoundation/Zope/issues/1220 -docutils = 0.21.2 -pygments = 2.14.0 -plone.app.linkintegrity = 4.0.3 -robotframework-browser = 17.5.2 -robotframework-assertion-engine = 2.0.0 -robotframework-debuglibrary = 2.3.0 -robotframework-pythonlibcore = 4.2.0 -grpcio-tools = 1.59.0 +twine = 5.1.1 diff --git a/plone-6.1.x.cfg b/plone-6.1.x.cfg index 54716fa951..10bfe28308 100644 --- a/plone-6.1.x.cfg +++ b/plone-6.1.x.cfg @@ -1,17 +1,10 @@ [buildout] extends = - https://dist.plone.org/release/6.1.0a3/versions.cfg + https://dist.plone.org/release/6.1.0b2/versions.cfg base.cfg -[buildout:python37] -parts = - test - code-analysis - [instance] recipe = plone.recipe.zope2instance zodb-temporary-storage = off [versions] -# Override pin from Zope. https://github.com/zopefoundation/Zope/issues/1220 -docutils = 0.21.2 diff --git a/requirements-6.0.txt b/requirements-6.0.txt index b654a46a97..d75ea5cca5 100644 --- a/requirements-6.0.txt +++ b/requirements-6.0.txt @@ -1 +1 @@ --r https://dist.plone.org/release/6.0.12/requirements.txt +-r https://dist.plone.org/release/6.0.14/requirements.txt diff --git a/requirements-6.1.txt b/requirements-6.1.txt index 7ce0be7bb3..1abfefefc7 100644 --- a/requirements-6.1.txt +++ b/requirements-6.1.txt @@ -1 +1 @@ --r https://dist.plone.org/release/6.1.0a3/requirements.txt +-r https://dist.plone.org/release/6.1.0b2/requirements.txt 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 f172b2c8d1..213ccce065 100644 --- a/src/plone/restapi/tests/http-examples/registry_get_list.resp +++ b/src/plone/restapi/tests/http-examples/registry_get_list.resp @@ -423,5 +423,5 @@ Content-Type: application/json "value": "The person that created an item" } ], - "items_total": 2973 + "items_total": 2974 } diff --git a/src/plone/restapi/tests/http-examples/site_get.resp b/src/plone/restapi/tests/http-examples/site_get.resp index 43a502cc1e..417c0cbf83 100644 --- a/src/plone/restapi/tests/http-examples/site_get.resp +++ b/src/plone/restapi/tests/http-examples/site_get.resp @@ -4,7 +4,7 @@ Content-Type: application/json { "@id": "http://localhost:55001/plone/@site", "features": { - "filter_aliases_by_date": false + "filter_aliases_by_date": true }, "plone.allowed_sizes": [ "huge 1600:65536", From 88f2bc9cba9d2f3057813f8e0b4bc029ddcee117 Mon Sep 17 00:00:00 2001 From: Faakhir Zahid <110815427+Faakhir30@users.noreply.github.com> Date: Wed, 22 Jan 2025 08:01:56 +0500 Subject: [PATCH 06/17] add query param to search registry records. (#1861) * add query param to search registry records. * refactor serializer. * format using black. * use tmp registry isntead of seperate serializer class. * udpate http resp files * update docs. * update docs. * version added * Update docs/source/endpoints/registry.md * Apply suggestions from code review --------- Co-authored-by: Steve Piercy Co-authored-by: David Glick --- docs/source/endpoints/registry.md | 19 +++++++ news/1861.feature | 1 + src/plone/restapi/services/registry/get.py | 13 ++++- .../registry_get_list_filtered.req | 3 + .../registry_get_list_filtered.resp | 57 +++++++++++++++++++ src/plone/restapi/tests/test_documentation.py | 4 ++ src/plone/restapi/tests/test_registry.py | 9 +++ 7 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 news/1861.feature create mode 100644 src/plone/restapi/tests/http-examples/registry_get_list_filtered.req create mode 100644 src/plone/restapi/tests/http-examples/registry_get_list_filtered.resp diff --git a/docs/source/endpoints/registry.md b/docs/source/endpoints/registry.md index 57fb9f3426..1a5e0a9c7f 100644 --- a/docs/source/endpoints/registry.md +++ b/docs/source/endpoints/registry.md @@ -52,6 +52,25 @@ Example response: :language: http ``` +## Filter list of registry records + +```{versionadded} plone.restapi 9.10.0 +``` + +You can filter a list of registry records and batch the results. +To do so, append a query string to the listing endpoint with a `q` parameter and its value set to the prefix of the desired record name. +See {doc}`../usage/batching` for details of how to work with batched results. + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/registry_get_list_filtered.req +``` + +Example response: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/registry_get_list_filtered.resp +:language: http +``` ## Updating registry records diff --git a/news/1861.feature b/news/1861.feature new file mode 100644 index 0000000000..5e3538d612 --- /dev/null +++ b/news/1861.feature @@ -0,0 +1 @@ +In the `@registry` endpoint, added support for filtering the list of registry records. @Faakhir30 \ No newline at end of file diff --git a/src/plone/restapi/services/registry/get.py b/src/plone/restapi/services/registry/get.py index b75ecc07a4..689d712da3 100644 --- a/src/plone/restapi/services/registry/get.py +++ b/src/plone/restapi/services/registry/get.py @@ -1,3 +1,4 @@ +from plone.registry import Registry from plone.registry.interfaces import IRegistry from plone.restapi.interfaces import ISerializeToJson from plone.restapi.serializer.converters import json_compatible @@ -35,5 +36,15 @@ def reply(self): value = registry[self._get_record_name] return json_compatible(value) else: # batched listing - serializer = getMultiAdapter((registry, self.request), ISerializeToJson) + if q := self.request.form.get("q"): + + tmp_registry = Registry() + for key in registry.records.keys(): + if key.startswith(q): + tmp_registry.records[key] = registry.records[key] + registry = tmp_registry + serializer = getMultiAdapter( + (registry, self.request), + ISerializeToJson, + ) return serializer() diff --git a/src/plone/restapi/tests/http-examples/registry_get_list_filtered.req b/src/plone/restapi/tests/http-examples/registry_get_list_filtered.req new file mode 100644 index 0000000000..f9cfd8b3c8 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/registry_get_list_filtered.req @@ -0,0 +1,3 @@ +GET /plone/@registry?q=Products.CMFPlone HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/registry_get_list_filtered.resp b/src/plone/restapi/tests/http-examples/registry_get_list_filtered.resp new file mode 100644 index 0000000000..8962fb5d70 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/registry_get_list_filtered.resp @@ -0,0 +1,57 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:55001/plone/@registry?q=Products.CMFPlone", + "items": [ + { + "name": "Products.CMFPlone.i18nl10n.override_dateformat.Enabled", + "schema": { + "properties": { + "description": "Override the translation machinery", + "factory": "Yes/No", + "title": "Enabled", + "type": "boolean" + } + }, + "value": false + }, + { + "name": "Products.CMFPlone.i18nl10n.override_dateformat.date_format_long", + "schema": { + "properties": { + "description": "Default value: %Y-%m-%d %H:%M (2038-01-19 03:14)", + "factory": "Text line (String)", + "title": "old ZMI property: localLongTimeFormat", + "type": "string" + } + }, + "value": "%Y-%m-%d %H:%M" + }, + { + "name": "Products.CMFPlone.i18nl10n.override_dateformat.date_format_short", + "schema": { + "properties": { + "description": "Default value: %Y-%m-%d (2038-01-19)", + "factory": "Text line (String)", + "title": "old ZMI property: localTimeFormat", + "type": "string" + } + }, + "value": "%Y-%m-%d" + }, + { + "name": "Products.CMFPlone.i18nl10n.override_dateformat.time_format", + "schema": { + "properties": { + "description": "Default value: %H:%M (03:14)", + "factory": "Text line (String)", + "title": "old ZMI property: localTimeOnlyFormat", + "type": "string" + } + }, + "value": "%H:%M" + } + ], + "items_total": 4 +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 574873ce6e..916f654b2a 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -549,6 +549,10 @@ def test_documentation_registry_get_list(self): response = self.api_session.get("/@registry") save_request_and_response_for_docs("registry_get_list", response) + def test_documentation_registry_get_list_filtered(self): + response = self.api_session.get("/@registry?q=Products.CMFPlone") + save_request_and_response_for_docs("registry_get_list_filtered", response) + def test_documentation_types(self): response = self.api_session.get("/@types") save_request_and_response_for_docs("types", response) diff --git a/src/plone/restapi/tests/test_registry.py b/src/plone/restapi/tests/test_registry.py index 61a266b644..069ca363cb 100644 --- a/src/plone/restapi/tests/test_registry.py +++ b/src/plone/restapi/tests/test_registry.py @@ -107,3 +107,12 @@ def test_get_listing(self): self.assertIn("items", response) self.assertIn("batching", response) self.assertIn("next", response["batching"]) + + def test_get_filtered_listing(self): + response = self.api_session.get("/@registry?q=foo.bar1") + self.assertEqual(response.status_code, 200) + response = response.json() + # 10 records from foo.bar10 to foo.bar19 and 1 record foo.bar1 + self.assertEqual(len(response["items"]), 11) + self.assertEqual(response["items"][0]["name"], "foo.bar1") + self.assertEqual(response["items"][0]["value"], "Lorem Ipsum") From 5a120c416db8bbd6168fc8c10fdf1ef75e5eac62 Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Wed, 22 Jan 2025 04:14:39 +0100 Subject: [PATCH 07/17] fix: contextnavigation when a file has an unknown type (#1864) * fix: contextnavigation when a file has an unknown type * changelog * Update news/1864.bugfix --------- Co-authored-by: David Glick --- news/1864.bugfix | 1 + .../restapi/services/contextnavigation/get.py | 3 +- .../tests/test_services_contextnavigation.py | 29 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 news/1864.bugfix diff --git a/news/1864.bugfix b/news/1864.bugfix new file mode 100644 index 0000000000..9e63a64abd --- /dev/null +++ b/news/1864.bugfix @@ -0,0 +1 @@ +In the `@contextnavigation` endpoint, return `"icon": null` for Files with a mimetype not found in the `content_type_registry`, instead of raising `TypeError`. @mamico diff --git a/src/plone/restapi/services/contextnavigation/get.py b/src/plone/restapi/services/contextnavigation/get.py index 0fe0d7a054..3f22602222 100644 --- a/src/plone/restapi/services/contextnavigation/get.py +++ b/src/plone/restapi/services/contextnavigation/get.py @@ -357,7 +357,8 @@ def getMimeTypeIcon(self, node): mtt = getToolByName(self.context, "mimetypes_registry") if fileo.contentType: ctype = mtt.lookup(fileo.contentType) - return os.path.join(portal_url, guess_icon_path(ctype[0])) + if ctype: + return os.path.join(portal_url, guess_icon_path(ctype[0])) except AttributeError: pass diff --git a/src/plone/restapi/tests/test_services_contextnavigation.py b/src/plone/restapi/tests/test_services_contextnavigation.py index 9f8405e95a..05fe3c037e 100644 --- a/src/plone/restapi/tests/test_services_contextnavigation.py +++ b/src/plone/restapi/tests/test_services_contextnavigation.py @@ -3,6 +3,7 @@ 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.namedfile.file import NamedBlobFile from plone.registry.interfaces import IRegistry from plone.restapi.services.contextnavigation.get import ContextNavigation from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING @@ -99,6 +100,9 @@ def populateSite(self): folder2.invokeFactory("Document", "doc22") folder2.invokeFactory("Document", "doc23") folder2.invokeFactory("File", "file21") + folder2.file21.file = NamedBlobFile( + data="Hello World", contentType="text/plain", filename="file.txt" + ) folder2.invokeFactory("Folder", "folder21") folder21 = getattr(folder2, "folder21") folder21.invokeFactory("Document", "doc211") @@ -996,3 +1000,28 @@ def testContextNavigation(self): "/plone/folder1/doc11", ) ) + + def testIcon(self): + view = self.renderer( + self.portal.folder2.file21, + opts(root_path="/folder2", topLevel=0), + ) + tree = view.getNavTree() + self.assertTrue(tree) + self.assertEqual( + tree["items"][0]["icon"], + "/plone/++resource++mimetype.icons/txt.png", + ) + + def testIconNotRegisteredMimetype(self): + self.portal.folder2.file21.file.contentType = "plain/x-text" + view = self.renderer( + self.portal.folder2.file21, + opts(root_path="/folder2", topLevel=0), + ) + tree = view.getNavTree() + self.assertTrue(tree) + self.assertEqual( + tree["items"][0]["icon"], + None, + ) From d54f9747ff2be681909aaee11f776381863dd7d2 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 24 Jan 2025 21:57:39 +0100 Subject: [PATCH 08/17] Prevent deprecation warnings. (#1863) --- news/4090.bugfix | 1 + src/plone/restapi/bbb.py | 10 ++++++++++ src/plone/restapi/configure.zcml | 6 +++--- src/plone/restapi/profiles/performance/registry.xml | 2 +- src/plone/restapi/search/handler.py | 9 +-------- src/plone/restapi/serializer/dxcontent.py | 2 +- src/plone/restapi/services/addons/configure.zcml | 2 +- src/plone/restapi/services/aliases/configure.zcml | 8 ++++---- src/plone/restapi/services/auth/configure.zcml | 8 ++++---- src/plone/restapi/services/content/configure.zcml | 12 ++++++------ src/plone/restapi/services/content/utils.py | 2 +- src/plone/restapi/services/contextnavigation/get.py | 10 +++++----- .../restapi/services/controlpanels/configure.zcml | 8 ++++---- src/plone/restapi/services/database/configure.zcml | 2 +- .../restapi/services/linkintegrity/configure.zcml | 2 +- src/plone/restapi/services/locking/configure.zcml | 8 ++++---- src/plone/restapi/services/navigation/get.py | 4 ++-- .../restapi/services/querysources/configure.zcml | 2 +- .../restapi/services/querystring/configure.zcml | 2 +- .../services/querystringsearch/configure.zcml | 4 ++-- src/plone/restapi/services/relations/configure.zcml | 6 +++--- src/plone/restapi/services/roles/configure.zcml | 2 +- src/plone/restapi/services/site/configure.zcml | 2 +- src/plone/restapi/services/site/get.py | 4 ++-- src/plone/restapi/services/system/configure.zcml | 2 +- .../restapi/services/transactions/configure.zcml | 4 ++-- src/plone/restapi/services/types/configure.zcml | 8 ++++---- src/plone/restapi/services/types/get.py | 2 +- src/plone/restapi/services/upgrade/configure.zcml | 4 ++-- .../restapi/services/vocabularies/configure.zcml | 2 +- src/plone/restapi/services/workflow/configure.zcml | 2 +- src/plone/restapi/tests/test_auth.py | 4 ++-- src/plone/restapi/tests/test_documentation.py | 5 ++--- src/plone/restapi/tests/test_search.py | 2 +- .../restapi/tests/test_services_contextnavigation.py | 2 +- src/plone/restapi/tests/test_services_navroot.py | 7 +++---- src/plone/restapi/upgrades/configure.zcml | 6 +++--- 37 files changed, 85 insertions(+), 83 deletions(-) create mode 100644 news/4090.bugfix diff --git a/news/4090.bugfix b/news/4090.bugfix new file mode 100644 index 0000000000..3a0ea37c4b --- /dev/null +++ b/news/4090.bugfix @@ -0,0 +1 @@ +Prevent deprecation warnings. @mauritsvanrees diff --git a/src/plone/restapi/bbb.py b/src/plone/restapi/bbb.py index 6e54f1d84e..bbe8c0faef 100644 --- a/src/plone/restapi/bbb.py +++ b/src/plone/restapi/bbb.py @@ -5,6 +5,8 @@ from plone.base.interfaces import IImagingSchema from plone.base.interfaces import ILanguage from plone.base.interfaces import IMailSchema + from plone.base.interfaces import IMigratingPloneSiteRoot + from plone.base.interfaces import INavigationRoot from plone.base.interfaces import INavigationSchema from plone.base.interfaces import INonInstallable from plone.base.interfaces import INonStructuralFolder @@ -13,18 +15,25 @@ from plone.base.interfaces import ISecuritySchema from plone.base.interfaces import ISelectableConstrainTypes from plone.base.interfaces import ISiteSchema + from plone.base.interfaces import ITestCasePloneSiteRoot + from plone.base.navigationroot import get_navigation_root from plone.base.utils import base_hasattr from plone.base.utils import safe_callable from plone.base.utils import safe_hasattr from plone.base.utils import safe_text except ImportError: # BBB Plone 5.2 + from plone.app.layout.navigation.interfaces import INavigationRoot + from plone.app.layout.navigation.root import ( + getNavigationRoot as get_navigation_root, + ) from Products.CMFPlone.defaultpage import is_default_page from Products.CMFPlone.interfaces import IConstrainTypes from Products.CMFPlone.interfaces import IEditingSchema from Products.CMFPlone.interfaces import IImagingSchema from Products.CMFPlone.interfaces import ILanguage from Products.CMFPlone.interfaces import IMailSchema + from Products.CMFPlone.interfaces import IMigratingPloneSiteRoot from Products.CMFPlone.interfaces import INavigationSchema from Products.CMFPlone.interfaces import INonInstallable from Products.CMFPlone.interfaces import INonStructuralFolder @@ -33,6 +42,7 @@ from Products.CMFPlone.interfaces import ISecuritySchema from Products.CMFPlone.interfaces import ISelectableConstrainTypes from Products.CMFPlone.interfaces import ISiteSchema + from Products.CMFPlone.interfaces import ITestCasePloneSiteRoot from Products.CMFPlone.utils import base_hasattr from Products.CMFPlone.utils import safe_callable from Products.CMFPlone.utils import safe_hasattr diff --git a/src/plone/restapi/configure.zcml b/src/plone/restapi/configure.zcml index 8bd6c38c33..6d6ab0bace 100644 --- a/src/plone/restapi/configure.zcml +++ b/src/plone/restapi/configure.zcml @@ -52,7 +52,7 @@ title="plone.restapi testing" description="Adds sample content types for testing" provides="Products.GenericSetup.interfaces.EXTENSION" - for="Products.CMFPlone.interfaces.ITestCasePloneSiteRoot" + for="plone.restapi.bbb.ITestCasePloneSiteRoot" directory="profiles/testing" /> @@ -61,7 +61,7 @@ title="plone.restapi testing-workflows" description="Adds sample workflows for testing" provides="Products.GenericSetup.interfaces.EXTENSION" - for="Products.CMFPlone.interfaces.ITestCasePloneSiteRoot" + for="plone.restapi.bbb.ITestCasePloneSiteRoot" directory="profiles/testing-workflows" /> @@ -127,7 +127,7 @@ diff --git a/src/plone/restapi/profiles/performance/registry.xml b/src/plone/restapi/profiles/performance/registry.xml index b95fcf8861..f169f73704 100644 --- a/src/plone/restapi/profiles/performance/registry.xml +++ b/src/plone/restapi/profiles/performance/registry.xml @@ -1,6 +1,6 @@ - + icon 32:32 tile 64:64 diff --git a/src/plone/restapi/search/handler.py b/src/plone/restapi/search/handler.py index 2a362e05d5..22dda01fb8 100644 --- a/src/plone/restapi/search/handler.py +++ b/src/plone/restapi/search/handler.py @@ -1,5 +1,6 @@ from plone.registry.interfaces import IRegistry from plone.restapi.bbb import ISearchSchema +from plone.restapi.bbb import get_navigation_root from plone.restapi.interfaces import ISerializeToJson from plone.restapi.interfaces import IZCatalogCompatibleQuery from Products.CMFCore.utils import getToolByName @@ -7,14 +8,6 @@ from zope.component import getUtility -try: - from plone.base.navigationroot import get_navigation_root -except ImportError: - from plone.app.layout.navigation.root import ( - getNavigationRoot as get_navigation_root, - ) - - class SearchHandler: """Executes a catalog search based on a query dict, and returns JSON compatible results. diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index eb0fc44c6e..30e0a970e0 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -7,6 +7,7 @@ from plone.dexterity.interfaces import IDexterityContent from plone.dexterity.utils import iterSchemata from plone.restapi.batching import HypermediaBatch +from plone.restapi.bbb import base_hasattr from plone.restapi.deserializer import boolean_value from plone.restapi.interfaces import IFieldSerializer from plone.restapi.interfaces import IObjectPrimaryFieldTarget @@ -21,7 +22,6 @@ from plone.rfc822.interfaces import IPrimaryFieldInfo from plone.supermodel.utils import mergedTaggedValueDict from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import base_hasattr from Products.CMFCore.interfaces import IContentish from zope.component import adapter from zope.component import ComponentLookupError diff --git a/src/plone/restapi/services/addons/configure.zcml b/src/plone/restapi/services/addons/configure.zcml index b8140dc1fb..e05ca6f114 100644 --- a/src/plone/restapi/services/addons/configure.zcml +++ b/src/plone/restapi/services/addons/configure.zcml @@ -14,7 +14,7 @@ diff --git a/src/plone/restapi/services/aliases/configure.zcml b/src/plone/restapi/services/aliases/configure.zcml index b68d1050cb..b3417c7f15 100644 --- a/src/plone/restapi/services/aliases/configure.zcml +++ b/src/plone/restapi/services/aliases/configure.zcml @@ -7,7 +7,7 @@ method="GET" accept="application/json,application/schema+json" factory=".get.AliasesGet" - for="Products.CMFPlone.interfaces.IPloneSiteRoot" + for="plone.restapi.bbb.IPloneSiteRoot" permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> @@ -16,7 +16,7 @@ method="GET" accept="text/csv" factory=".get.AliasesGet" - for="Products.CMFPlone.interfaces.IPloneSiteRoot" + for="plone.restapi.bbb.IPloneSiteRoot" permission="Products.CMFPlone.ManagePortalAliases" name="@aliases" /> @@ -41,7 +41,7 @@ @@ -57,7 +57,7 @@ diff --git a/src/plone/restapi/services/auth/configure.zcml b/src/plone/restapi/services/auth/configure.zcml index f5604d81f2..96607f4fb7 100644 --- a/src/plone/restapi/services/auth/configure.zcml +++ b/src/plone/restapi/services/auth/configure.zcml @@ -6,7 +6,7 @@ @@ -14,7 +14,7 @@ @@ -22,7 +22,7 @@ @@ -30,7 +30,7 @@ diff --git a/src/plone/restapi/services/content/configure.zcml b/src/plone/restapi/services/content/configure.zcml index 72073faafb..674d54523b 100644 --- a/src/plone/restapi/services/content/configure.zcml +++ b/src/plone/restapi/services/content/configure.zcml @@ -8,7 +8,7 @@ @@ -27,7 +27,7 @@ @@ -55,7 +55,7 @@ @@ -71,7 +71,7 @@ @@ -119,7 +119,7 @@ @@ -135,7 +135,7 @@ diff --git a/src/plone/restapi/services/content/utils.py b/src/plone/restapi/services/content/utils.py index beb23b98ef..a2d958990a 100644 --- a/src/plone/restapi/services/content/utils.py +++ b/src/plone/restapi/services/content/utils.py @@ -2,9 +2,9 @@ from DateTime import DateTime from plone.app.content.interfaces import INameFromTitle from plone.app.uuid.utils import uuidToObject +from plone.restapi.bbb import base_hasattr from plone.uuid.interfaces import IUUID from Products.CMFCore.utils import getToolByName -from Products.CMFPlone.utils import base_hasattr from random import randint from zExceptions import Unauthorized from zope.component import getUtility diff --git a/src/plone/restapi/services/contextnavigation/get.py b/src/plone/restapi/services/contextnavigation/get.py index 3f22602222..c312ea7b05 100644 --- a/src/plone/restapi/services/contextnavigation/get.py +++ b/src/plone/restapi/services/contextnavigation/get.py @@ -6,12 +6,12 @@ from Acquisition import aq_parent from collections import UserDict from plone import api -from plone.app.layout.navigation.interfaces import INavigationRoot from plone.app.layout.navigation.navtree import buildFolderTree -from plone.app.layout.navigation.root import getNavigationRoot +from plone.restapi.bbb import get_navigation_root from plone.i18n.normalizer.interfaces import IIDNormalizer from plone.memoize.instance import memoize from plone.registry.interfaces import IRegistry +from plone.restapi.bbb import INavigationRoot from plone.restapi.bbb import INavigationSchema from plone.restapi.bbb import INonStructuralFolder from plone.restapi.bbb import is_default_page @@ -353,7 +353,7 @@ def getMimeTypeIcon(self, node): if not node["normalized_portal_type"] == "file": return fileo = node["item"].getObject().file - portal_url = getNavigationRoot(self.context) + portal_url = get_navigation_root(self.context) mtt = getToolByName(self.context, "mimetypes_registry") if fileo.contentType: ctype = mtt.lookup(fileo.contentType) @@ -563,7 +563,7 @@ def getRootPath(context, currentFolderOnly, topLevel, root_path): if root is not None: rootPath = "/".join(root.getPhysicalPath()) else: - rootPath = getNavigationRoot(context) + rootPath = get_navigation_root(context) # Adjust for topLevel if topLevel > 0: @@ -638,7 +638,7 @@ def __init__(self, context, data): if root is not None: rootPath = "/".join(root.getPhysicalPath()) else: - rootPath = getNavigationRoot(context) + rootPath = get_navigation_root(context) currentPath = "/".join(context.getPhysicalPath()) diff --git a/src/plone/restapi/services/controlpanels/configure.zcml b/src/plone/restapi/services/controlpanels/configure.zcml index 690490e2aa..beb718fe7a 100644 --- a/src/plone/restapi/services/controlpanels/configure.zcml +++ b/src/plone/restapi/services/controlpanels/configure.zcml @@ -19,7 +19,7 @@ @@ -27,7 +27,7 @@ @@ -35,7 +35,7 @@ @@ -43,7 +43,7 @@ diff --git a/src/plone/restapi/services/database/configure.zcml b/src/plone/restapi/services/database/configure.zcml index a4477cc3b0..428bcb5373 100644 --- a/src/plone/restapi/services/database/configure.zcml +++ b/src/plone/restapi/services/database/configure.zcml @@ -19,7 +19,7 @@ diff --git a/src/plone/restapi/services/linkintegrity/configure.zcml b/src/plone/restapi/services/linkintegrity/configure.zcml index 65da4559e8..377f17de3b 100644 --- a/src/plone/restapi/services/linkintegrity/configure.zcml +++ b/src/plone/restapi/services/linkintegrity/configure.zcml @@ -9,7 +9,7 @@ diff --git a/src/plone/restapi/services/locking/configure.zcml b/src/plone/restapi/services/locking/configure.zcml index abfd584a77..1c0b10a82a 100644 --- a/src/plone/restapi/services/locking/configure.zcml +++ b/src/plone/restapi/services/locking/configure.zcml @@ -14,7 +14,7 @@ @@ -30,7 +30,7 @@ @@ -46,7 +46,7 @@ @@ -62,7 +62,7 @@ diff --git a/src/plone/restapi/services/navigation/get.py b/src/plone/restapi/services/navigation/get.py index 96748f336f..c70ca51a8e 100644 --- a/src/plone/restapi/services/navigation/get.py +++ b/src/plone/restapi/services/navigation/get.py @@ -1,6 +1,6 @@ from Acquisition import aq_inner from collections import defaultdict -from plone.app.layout.navigation.root import getNavigationRoot +from plone.restapi.bbb import get_navigation_root from plone.memoize.view import memoize from plone.memoize.view import memoize_contextless from plone.registry.interfaces import IRegistry @@ -60,7 +60,7 @@ def default_language(self): @property def navtree_path(self): - return getNavigationRoot(self.context) + return get_navigation_root(self.context) @property def current_language(self): diff --git a/src/plone/restapi/services/querysources/configure.zcml b/src/plone/restapi/services/querysources/configure.zcml index e44dfab15d..cb807d94d0 100644 --- a/src/plone/restapi/services/querysources/configure.zcml +++ b/src/plone/restapi/services/querysources/configure.zcml @@ -7,7 +7,7 @@ method="GET" accept="application/json" factory=".get.QuerySourcesGet" - for="Products.CMFPlone.interfaces.IPloneSiteRoot" + for="plone.restapi.bbb.IPloneSiteRoot" permission="plone.restapi.vocabularies" name="@querysources" /> diff --git a/src/plone/restapi/services/querystring/configure.zcml b/src/plone/restapi/services/querystring/configure.zcml index f7e6f38343..c280558a72 100644 --- a/src/plone/restapi/services/querystring/configure.zcml +++ b/src/plone/restapi/services/querystring/configure.zcml @@ -8,7 +8,7 @@ diff --git a/src/plone/restapi/services/querystringsearch/configure.zcml b/src/plone/restapi/services/querystringsearch/configure.zcml index 21ad182d55..35a690c2a6 100644 --- a/src/plone/restapi/services/querystringsearch/configure.zcml +++ b/src/plone/restapi/services/querystringsearch/configure.zcml @@ -8,7 +8,7 @@ @@ -24,7 +24,7 @@ diff --git a/src/plone/restapi/services/relations/configure.zcml b/src/plone/restapi/services/relations/configure.zcml index f21cd0ac15..fb7e9d5e17 100644 --- a/src/plone/restapi/services/relations/configure.zcml +++ b/src/plone/restapi/services/relations/configure.zcml @@ -7,7 +7,7 @@ method="GET" accept="application/json,application/schema+json" factory=".get.GetRelations" - for="Products.CMFPlone.interfaces.IPloneSiteRoot" + for="plone.restapi.bbb.IPloneSiteRoot" permission="zope2.View" name="@relations" /> @@ -15,7 +15,7 @@ @@ -23,7 +23,7 @@ diff --git a/src/plone/restapi/services/roles/configure.zcml b/src/plone/restapi/services/roles/configure.zcml index 4dfd989234..e903274aca 100644 --- a/src/plone/restapi/services/roles/configure.zcml +++ b/src/plone/restapi/services/roles/configure.zcml @@ -6,7 +6,7 @@ diff --git a/src/plone/restapi/services/site/configure.zcml b/src/plone/restapi/services/site/configure.zcml index 24ed968654..87c65903b6 100644 --- a/src/plone/restapi/services/site/configure.zcml +++ b/src/plone/restapi/services/site/configure.zcml @@ -8,7 +8,7 @@ diff --git a/src/plone/restapi/services/site/get.py b/src/plone/restapi/services/site/get.py index 4ab0a9801e..b6f0c5a803 100644 --- a/src/plone/restapi/services/site/get.py +++ b/src/plone/restapi/services/site/get.py @@ -5,10 +5,10 @@ from plone.event.utils import validated_timezone from plone.i18n.interfaces import ILanguageSchema from plone.registry.interfaces import IRegistry +from plone.restapi.bbb import IImagingSchema +from plone.restapi.bbb import ISiteSchema from plone.restapi.interfaces import IExpandableElement from plone.restapi.services import Service -from Products.CMFPlone.interfaces import IImagingSchema -from Products.CMFPlone.interfaces import ISiteSchema from Products.CMFPlone.utils import getSiteLogo from Products.CMFPlone.controlpanel.browser.redirects import RedirectionSet from zope.component import adapter diff --git a/src/plone/restapi/services/system/configure.zcml b/src/plone/restapi/services/system/configure.zcml index bbf1900010..9258c0aa52 100644 --- a/src/plone/restapi/services/system/configure.zcml +++ b/src/plone/restapi/services/system/configure.zcml @@ -6,7 +6,7 @@ diff --git a/src/plone/restapi/services/transactions/configure.zcml b/src/plone/restapi/services/transactions/configure.zcml index 1a44d018b7..6117fc3aaf 100644 --- a/src/plone/restapi/services/transactions/configure.zcml +++ b/src/plone/restapi/services/transactions/configure.zcml @@ -9,7 +9,7 @@ @@ -17,7 +17,7 @@ diff --git a/src/plone/restapi/services/types/configure.zcml b/src/plone/restapi/services/types/configure.zcml index a386e9b159..f91bbca5fa 100644 --- a/src/plone/restapi/services/types/configure.zcml +++ b/src/plone/restapi/services/types/configure.zcml @@ -15,7 +15,7 @@ @@ -23,7 +23,7 @@ @@ -31,7 +31,7 @@ @@ -39,7 +39,7 @@ diff --git a/src/plone/restapi/services/types/get.py b/src/plone/restapi/services/types/get.py index af20cdd01f..777e9379eb 100644 --- a/src/plone/restapi/services/types/get.py +++ b/src/plone/restapi/services/types/get.py @@ -1,5 +1,6 @@ from plone.dexterity.interfaces import IDexterityContent from plone.restapi.bbb import IConstrainTypes +from plone.restapi.bbb import IPloneSiteRoot from plone.restapi.interfaces import IExpandableElement from plone.restapi.interfaces import IPloneRestapiLayer from plone.restapi.services import Service @@ -7,7 +8,6 @@ from plone.restapi.types.utils import get_info_for_fieldset from plone.restapi.types.utils import get_info_for_type from Products.CMFCore.interfaces import IFolderish -from Products.CMFPlone.interfaces import IPloneSiteRoot from Products.CMFCore.utils import getToolByName from zExceptions import Unauthorized from zope.component import adapter diff --git a/src/plone/restapi/services/upgrade/configure.zcml b/src/plone/restapi/services/upgrade/configure.zcml index 5079d7fedb..9abc266b84 100644 --- a/src/plone/restapi/services/upgrade/configure.zcml +++ b/src/plone/restapi/services/upgrade/configure.zcml @@ -6,7 +6,7 @@ @@ -14,7 +14,7 @@ diff --git a/src/plone/restapi/services/vocabularies/configure.zcml b/src/plone/restapi/services/vocabularies/configure.zcml index 8091472727..295fc46de3 100644 --- a/src/plone/restapi/services/vocabularies/configure.zcml +++ b/src/plone/restapi/services/vocabularies/configure.zcml @@ -7,7 +7,7 @@ method="GET" accept="application/json" factory=".get.VocabulariesGet" - for="Products.CMFPlone.interfaces.IPloneSiteRoot" + for="plone.restapi.bbb.IPloneSiteRoot" permission="zope2.View" name="@vocabularies" /> diff --git a/src/plone/restapi/services/workflow/configure.zcml b/src/plone/restapi/services/workflow/configure.zcml index 29cb0163bf..a9ab252218 100644 --- a/src/plone/restapi/services/workflow/configure.zcml +++ b/src/plone/restapi/services/workflow/configure.zcml @@ -7,7 +7,7 @@ diff --git a/src/plone/restapi/tests/test_auth.py b/src/plone/restapi/tests/test_auth.py index e7df8470bb..23ed49640f 100644 --- a/src/plone/restapi/tests/test_auth.py +++ b/src/plone/restapi/tests/test_auth.py @@ -1,6 +1,8 @@ from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_PASSWORD +from plone.restapi.bbb import IPloneSiteRoot +from plone.restapi.interfaces import ILoginProviders from plone.restapi.permissions import UseRESTAPI from plone.restapi.testing import PLONE_RESTAPI_DX_INTEGRATION_TESTING from unittest import TestCase @@ -8,8 +10,6 @@ from zope.event import notify from ZPublisher.pubevents import PubStart from zope.component import provideAdapter -from plone.restapi.interfaces import ILoginProviders -from Products.CMFPlone.interfaces import IPloneSiteRoot class TestLogin(TestCase): diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 916f654b2a..5f2c92d97d 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -21,6 +21,8 @@ from plone.namedfile.file import NamedBlobFile from plone.namedfile.file import NamedBlobImage from plone.registry.interfaces import IRegistry +from plone.restapi.bbb import IPloneSiteRoot +from plone.restapi.interfaces import ILoginProviders from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING from plone.restapi.testing import PLONE_RESTAPI_ITERATE_FUNCTIONAL_TESTING @@ -43,9 +45,6 @@ from plone.app.testing import pushGlobalRegistry from plone.restapi.testing import register_static_uuid_utility from zope.component import provideAdapter -from plone.restapi.interfaces import ILoginProviders -from Products.CMFPlone.interfaces import IPloneSiteRoot - import collections import json diff --git a/src/plone/restapi/tests/test_search.py b/src/plone/restapi/tests/test_search.py index 84b6e0b480..471340994f 100644 --- a/src/plone/restapi/tests/test_search.py +++ b/src/plone/restapi/tests/test_search.py @@ -4,13 +4,13 @@ from pkg_resources import parse_version from plone import api from plone.app.discussion.interfaces import IDiscussionSettings -from plone.app.layout.navigation.interfaces import INavigationRoot from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD from plone.app.testing import TEST_USER_PASSWORD from plone.app.textfield.value import RichTextValue from plone.dexterity.utils import createContentInContainer from plone.registry.interfaces import IRegistry +from plone.restapi.bbb import INavigationRoot from plone.restapi.search.query import ZCatalogCompatibleQueryAdapter from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import RelativeSession diff --git a/src/plone/restapi/tests/test_services_contextnavigation.py b/src/plone/restapi/tests/test_services_contextnavigation.py index 05fe3c037e..334ce95f17 100644 --- a/src/plone/restapi/tests/test_services_contextnavigation.py +++ b/src/plone/restapi/tests/test_services_contextnavigation.py @@ -1,10 +1,10 @@ -from plone.app.layout.navigation.interfaces import INavigationRoot 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.namedfile.file import NamedBlobFile from plone.registry.interfaces import IRegistry +from plone.restapi.bbb import INavigationRoot from plone.restapi.services.contextnavigation.get import ContextNavigation from plone.restapi.testing import PLONE_RESTAPI_DX_FUNCTIONAL_TESTING from plone.restapi.testing import RelativeSession diff --git a/src/plone/restapi/tests/test_services_navroot.py b/src/plone/restapi/tests/test_services_navroot.py index 327b48847b..6f447c1063 100644 --- a/src/plone/restapi/tests/test_services_navroot.py +++ b/src/plone/restapi/tests/test_services_navroot.py @@ -1,6 +1,7 @@ -# -*- coding: utf-8 -*- +from plone import api from plone.app.testing import setRoles from plone.app.testing import TEST_USER_ID +from plone.restapi.bbb import INavigationRoot from plone.restapi.testing import ( PLONE_RESTAPI_DX_FUNCTIONAL_TESTING, PLONE_RESTAPI_DX_PAM_FUNCTIONAL_TESTING, @@ -8,11 +9,9 @@ from plone.restapi.testing import RelativeSession from zope.component import getMultiAdapter from zope.interface import alsoProvides -from plone.app.layout.navigation.interfaces import INavigationRoot -import unittest -from plone import api import transaction +import unittest class TestServicesNavroot(unittest.TestCase): diff --git a/src/plone/restapi/upgrades/configure.zcml b/src/plone/restapi/upgrades/configure.zcml index ab6cae887d..aeae9a0297 100644 --- a/src/plone/restapi/upgrades/configure.zcml +++ b/src/plone/restapi/upgrades/configure.zcml @@ -8,7 +8,7 @@ @@ -28,7 +28,7 @@ title="plone.restapi.upgrades.0002" description="" provides="Products.GenericSetup.interfaces.EXTENSION" - for="Products.CMFPlone.interfaces.IMigratingPloneSiteRoot" + for="plone.restapi.bbb.IMigratingPloneSiteRoot" directory="profiles/0002" /> @@ -47,7 +47,7 @@ title="plone.restapi.upgrades.0004" description="" provides="Products.GenericSetup.interfaces.EXTENSION" - for="Products.CMFPlone.interfaces.IMigratingPloneSiteRoot" + for="plone.restapi.bbb.IMigratingPloneSiteRoot" directory="profiles/0004" /> From a1ca992283db85842e23248e6c71596eafffa917 Mon Sep 17 00:00:00 2001 From: Steve Piercy Date: Sat, 25 Jan 2025 01:56:59 -0800 Subject: [PATCH 09/17] Linkcheck fixes (#1870) --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 043663c1c4..31fe20fa47 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,6 +1,6 @@ # .readthedocs.yaml # Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# See https://docs.readthedocs.com/platform/stable/config-file/v2.html for details # Required version: 2 From 3830080c59daf497880046022521ab8cdb10ee7a Mon Sep 17 00:00:00 2001 From: Mauro Amico Date: Mon, 27 Jan 2025 17:48:48 +0100 Subject: [PATCH 10/17] AttributeError occurs in creator_name when the user is missing (#1867) * AttributeError occurs in creator_name when the user is missing * changelog * userid * Update news/1867.bugfix --------- Co-authored-by: David Glick --- news/1867.bugfix | 1 + .../restapi/services/locking/__init__.py | 13 +++--- src/plone/restapi/tests/test_locking.py | 43 +++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 news/1867.bugfix diff --git a/news/1867.bugfix b/news/1867.bugfix new file mode 100644 index 0000000000..f4ddcaac89 --- /dev/null +++ b/news/1867.bugfix @@ -0,0 +1 @@ +In the `@locking` endpoint, fixed edge cases where the user who owns the lock was not found correctly. @mamico diff --git a/src/plone/restapi/services/locking/__init__.py b/src/plone/restapi/services/locking/__init__.py index 1b22e975a6..c9347a7b4b 100644 --- a/src/plone/restapi/services/locking/__init__.py +++ b/src/plone/restapi/services/locking/__init__.py @@ -7,14 +7,17 @@ from plone.locking.interfaces import ILockable -def creator_name(username): - user = api.user.get(username=username) - return user.getProperty("fullname") or username +def creator_name(userid): + user = api.user.get(userid=userid) + if user: + return user.getProperty("fullname") or userid + else: + return userid -def creator_url(username): +def creator_url(userid): url = api.portal.get().absolute_url() - return f"{url}/author/{username}" + return f"{url}/author/{userid}" def creation_date(timestamp): diff --git a/src/plone/restapi/tests/test_locking.py b/src/plone/restapi/tests/test_locking.py index 191b4d6bd8..40e7fc5b87 100644 --- a/src/plone/restapi/tests/test_locking.py +++ b/src/plone/restapi/tests/test_locking.py @@ -1,3 +1,4 @@ +from plone import api from plone.app.testing import login from plone.app.testing import SITE_OWNER_NAME from plone.app.testing import SITE_OWNER_PASSWORD @@ -112,3 +113,45 @@ def test_update_locked_object_with_token_succeeds(self): transaction.commit() self.assertEqual(response.status_code, 204) self.assertEqual(self.doc.Title(), "New Title") + + def test_lock_user_removed(self): + lockable = ILockable(self.doc) + api.user.create( + username="foo", + email="foo@bar.com", + roles=["Manager"], + ) + with api.env.adopt_user(username="foo"): + lockable.lock() + api.user.delete(username="foo") + transaction.commit() + # the user that locked the object is no longer present + response = self.api_session.get("/@lock") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["creator"], "foo") + self.assertEqual(response.json()["creator_name"], "foo") + self.assertTrue(lockable.locked()) + + def test_lock_username_vs_userid(self): + lockable = ILockable(self.doc) + api.user.create( + username="foo1234", + email="foo@bar.com", + roles=["Manager"], + properties={"fullname": "Foo Bar"}, + ) + pas = api.portal.get_tool("acl_users") + # generally the username and userid are the same... + self.assertEqual(pas.getUserById("foo1234").getUserName(), "foo1234") + # ...but we can change it + pas.updateLoginName("foo1234", "foo") + self.assertEqual(pas.getUserById("foo1234").getUserName(), "foo") + with api.env.adopt_user(username="foo"): + lockable.lock() + transaction.commit() + response = self.api_session.get("/@lock") + self.assertEqual(response.status_code, 200) + # here the userid + self.assertEqual(response.json()["creator"], "foo1234") + self.assertEqual(response.json()["creator_name"], "Foo Bar") + self.assertTrue(lockable.locked()) From d4573a40fed74f2cafee501810b2ecf7aae17d8c Mon Sep 17 00:00:00 2001 From: David Glick Date: Mon, 27 Jan 2025 20:21:28 -0800 Subject: [PATCH 11/17] Preparing release 9.10.0 --- CHANGES.rst | 27 +++++++++++++++++++++++++++ news/1685.internal | 1 - news/1757.feature | 1 - news/1855.bugfix | 1 - news/1857.bugfix | 1 - news/1858.bugfix | 1 - news/1861.feature | 1 - news/1864.bugfix | 1 - news/1867.bugfix | 1 - news/4090.bugfix | 1 - setup.py | 2 +- 11 files changed, 28 insertions(+), 10 deletions(-) delete mode 100644 news/1685.internal delete mode 100644 news/1757.feature delete mode 100644 news/1855.bugfix delete mode 100644 news/1857.bugfix delete mode 100644 news/1858.bugfix delete mode 100644 news/1861.feature delete mode 100644 news/1864.bugfix delete mode 100644 news/1867.bugfix delete mode 100644 news/4090.bugfix diff --git a/CHANGES.rst b/CHANGES.rst index ea84b6bbac..635a491e90 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,33 @@ Changelog .. towncrier release notes start +9.10.0 (2025-01-27) +------------------- + +New features: + + +- Add a `@login` endpoint to get external login services' links. @erral (#1757) +- In the `@registry` endpoint, added support for filtering the list of registry records. @Faakhir30 (#1861) + + +Bug fixes: + + +- Changed bad int inputs into 500 Exceptions to 400 BadRequest so they can be filtered out of logs more easily. @djay (#1855) +- Handle TypeError on querystringsearch as BadRequest. @djay (#1857) +- Add parse_int to handle all cases of BadRequests from ints passed in. @djay (#1858) +- In the `@contextnavigation` endpoint, return `"icon": null` for Files with a mimetype not found in the `content_type_registry`, instead of raising `TypeError`. @mamico (#1864) +- In the `@locking` endpoint, fixed edge cases where the user who owns the lock was not found correctly. @mamico (#1867) +- Prevent deprecation warnings. @mauritsvanrees (#4090) + + +Internal: + + +- Update CI. @davisagli (#1685) + + 9.9.0 (2024-12-18) ------------------ diff --git a/news/1685.internal b/news/1685.internal deleted file mode 100644 index 6fe9f0e0dc..0000000000 --- a/news/1685.internal +++ /dev/null @@ -1 +0,0 @@ -Update CI. @davisagli diff --git a/news/1757.feature b/news/1757.feature deleted file mode 100644 index c678441b4e..0000000000 --- a/news/1757.feature +++ /dev/null @@ -1 +0,0 @@ -Add a `@login` endpoint to get external login services' links. @erral diff --git a/news/1855.bugfix b/news/1855.bugfix deleted file mode 100644 index b164e9f037..0000000000 --- a/news/1855.bugfix +++ /dev/null @@ -1 +0,0 @@ -Changed bad int inputs into 500 Exceptions to 400 BadRequest so they can be filtered out of logs more easily. @djay \ No newline at end of file diff --git a/news/1857.bugfix b/news/1857.bugfix deleted file mode 100644 index ea4ba7b398..0000000000 --- a/news/1857.bugfix +++ /dev/null @@ -1 +0,0 @@ -Handle TypeError on querystringsearch as BadRequest. @djay \ No newline at end of file diff --git a/news/1858.bugfix b/news/1858.bugfix deleted file mode 100644 index b275567243..0000000000 --- a/news/1858.bugfix +++ /dev/null @@ -1 +0,0 @@ -Add parse_int to handle all cases of BadRequests from ints passed in. @djay \ No newline at end of file diff --git a/news/1861.feature b/news/1861.feature deleted file mode 100644 index 5e3538d612..0000000000 --- a/news/1861.feature +++ /dev/null @@ -1 +0,0 @@ -In the `@registry` endpoint, added support for filtering the list of registry records. @Faakhir30 \ No newline at end of file diff --git a/news/1864.bugfix b/news/1864.bugfix deleted file mode 100644 index 9e63a64abd..0000000000 --- a/news/1864.bugfix +++ /dev/null @@ -1 +0,0 @@ -In the `@contextnavigation` endpoint, return `"icon": null` for Files with a mimetype not found in the `content_type_registry`, instead of raising `TypeError`. @mamico diff --git a/news/1867.bugfix b/news/1867.bugfix deleted file mode 100644 index f4ddcaac89..0000000000 --- a/news/1867.bugfix +++ /dev/null @@ -1 +0,0 @@ -In the `@locking` endpoint, fixed edge cases where the user who owns the lock was not found correctly. @mamico diff --git a/news/4090.bugfix b/news/4090.bugfix deleted file mode 100644 index 3a0ea37c4b..0000000000 --- a/news/4090.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent deprecation warnings. @mauritsvanrees diff --git a/setup.py b/setup.py index 15942cd754..f5db188605 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.9.1.dev0" +version = "9.10.0" if sys.version_info.major == 2: raise ValueError( From 0a2f821690898c14cd2ceddd372088f748671ccd Mon Sep 17 00:00:00 2001 From: David Glick Date: Mon, 27 Jan 2025 20:22:19 -0800 Subject: [PATCH 12/17] Back to development: 9.10.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f5db188605..c0ba91e1ad 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.10.0" +version = "9.10.1.dev0" if sys.version_info.major == 2: raise ValueError( From 259d9691d6f0024ae717017f8f48f0ce5229d1e0 Mon Sep 17 00:00:00 2001 From: Wesley Barroso Lopes Date: Tue, 28 Jan 2025 15:01:55 -0300 Subject: [PATCH 13/17] Allows working copy of the Portal (#1823) * Allows working copy of the Portal Allows working copy services to be accessed in the Portal. Also returns working copy data in the Portal serialization. * Add Changes * Does not return working copy information when serializing Portal in Plone 5.2 * better check for working copy support: test for feature instead of testing for version * Update news/1823.feature --------- Co-authored-by: David Glick --- news/1823.feature | 1 + src/plone/restapi/serializer/dxcontent.py | 19 ++++++++--- src/plone/restapi/serializer/site.py | 5 +++ .../services/workingcopy/configure.zcml | 33 +++++++++++++++++++ 4 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 news/1823.feature diff --git a/news/1823.feature b/news/1823.feature new file mode 100644 index 0000000000..bdb1e94c48 --- /dev/null +++ b/news/1823.feature @@ -0,0 +1 @@ +Support working copies of the Plone Site. This feature is available when using `plone.app.iterate` 6.1.0 or later. @wesleybl diff --git a/src/plone/restapi/serializer/dxcontent.py b/src/plone/restapi/serializer/dxcontent.py index 30e0a970e0..f681e4a7d1 100644 --- a/src/plone/restapi/serializer/dxcontent.py +++ b/src/plone/restapi/serializer/dxcontent.py @@ -42,6 +42,19 @@ WorkingCopyInfo = None +def update_with_working_copy_info(context, result): + if WorkingCopyInfo is None: + return + + working_copy_info = WorkingCopyInfo(context) + try: + baseline, working_copy = working_copy_info.get_working_copy_info() + except TypeError: + # not supported for this content type + return + result.update({"working_copy": working_copy, "working_copy_of": baseline}) + + def get_allow_discussion_value(context, request, result): # This test is to handle the situation of plone.app.discussion not being installed # or not being activated. @@ -108,11 +121,7 @@ def __call__(self, version=None, include_items=True): result.update({"previous_item": {}, "next_item": {}}) # Insert working copy information - if WorkingCopyInfo is not None: - baseline, working_copy = WorkingCopyInfo( - self.context - ).get_working_copy_info() - result.update({"working_copy": working_copy, "working_copy_of": baseline}) + update_with_working_copy_info(self.context, result) # Insert locking information result.update({"lock": lock_info(obj)}) diff --git a/src/plone/restapi/serializer/site.py b/src/plone/restapi/serializer/site.py index 65081fd2ea..bb08759c77 100644 --- a/src/plone/restapi/serializer/site.py +++ b/src/plone/restapi/serializer/site.py @@ -12,6 +12,7 @@ from plone.restapi.interfaces import ISerializeToJsonSummary from plone.restapi.serializer.converters import json_compatible from plone.restapi.serializer.dxcontent import get_allow_discussion_value +from plone.restapi.serializer.dxcontent import update_with_working_copy_info from plone.restapi.serializer.expansion import expandable_elements from plone.restapi.serializer.utils import get_portal_type_title from plone.restapi.services.locking import lock_info @@ -26,6 +27,7 @@ from zope.schema import getFields from zope.security.interfaces import IPermission + import json @@ -74,6 +76,9 @@ def __call__(self, version=None): "description": self.context.description, } + # Insert working copy information + update_with_working_copy_info(self.context, result) + if HAS_PLONE_6: result["UID"] = self.context.UID() # Insert review_state diff --git a/src/plone/restapi/services/workingcopy/configure.zcml b/src/plone/restapi/services/workingcopy/configure.zcml index 431b6df84b..8b55b902e8 100644 --- a/src/plone/restapi/services/workingcopy/configure.zcml +++ b/src/plone/restapi/services/workingcopy/configure.zcml @@ -39,4 +39,37 @@ name="@workingcopy" /> + + + + + + + + + From 52910087081fc7994d629901e21c76b734ca61b1 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 31 Jan 2025 09:23:18 +0100 Subject: [PATCH 14/17] Bumped version for feature release. [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c0ba91e1ad..65059ee5b8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.10.1.dev0" +version = "9.11.0.dev0" if sys.version_info.major == 2: raise ValueError( From 553af566e1bb2996f5179857178a48fbd5e2d38c Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 31 Jan 2025 09:23:40 +0100 Subject: [PATCH 15/17] Preparing release 9.11.0 [ci skip] --- CHANGES.rst | 9 +++++++++ news/1823.feature | 1 - setup.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/1823.feature diff --git a/CHANGES.rst b/CHANGES.rst index 635a491e90..607bfef997 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -8,6 +8,15 @@ Changelog .. towncrier release notes start +9.11.0 (2025-01-31) +------------------- + +New features: + + +- Support working copies of the Plone Site. This feature is available when using `plone.app.iterate` 6.1.0 or later. @wesleybl (#1823) + + 9.10.0 (2025-01-27) ------------------- diff --git a/news/1823.feature b/news/1823.feature deleted file mode 100644 index bdb1e94c48..0000000000 --- a/news/1823.feature +++ /dev/null @@ -1 +0,0 @@ -Support working copies of the Plone Site. This feature is available when using `plone.app.iterate` 6.1.0 or later. @wesleybl diff --git a/setup.py b/setup.py index 65059ee5b8..6284a87fde 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.11.0.dev0" +version = "9.11.0" if sys.version_info.major == 2: raise ValueError( From 722fe2c36d75323140b79574d9d336512e10c0cb Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 31 Jan 2025 09:25:11 +0100 Subject: [PATCH 16/17] Back to development: 9.11.1 [ci skip] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6284a87fde..97146b4a69 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ import sys -version = "9.11.0" +version = "9.11.1.dev0" if sys.version_info.major == 2: raise ValueError( From bc29368009dfc0719adab723cfddd9675a3c79ea Mon Sep 17 00:00:00 2001 From: Mikel Larreategi Date: Sun, 2 Feb 2025 06:15:48 +0100 Subject: [PATCH 17/17] feat: add a @userschema/registration endpoint (#1874) * feat: add a @registration-userschema endpoint * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update docs/source/endpoints/userschema.md Co-authored-by: Steve Piercy * Update news/1873.feature Co-authored-by: Steve Piercy * change changlog * change changlog * Update news/1873.feature Co-authored-by: Steve Piercy * Update src/plone/restapi/services/userschema/configure.zcml Co-authored-by: Steve Piercy * Update src/plone/restapi/tests/test_documentation.py Co-authored-by: Steve Piercy * Update src/plone/restapi/tests/test_services_userschema.py Co-authored-by: Steve Piercy * rename test files * rename * Remove misleading note about Plone 5 * Clarify profile schema vs registration schema --------- Co-authored-by: Steve Piercy Co-authored-by: David Glick --- docs/source/endpoints/userschema.md | 31 ++++++-- news/1873.feature | 1 + src/plone/restapi/services/userschema/user.py | 31 +++++++- .../http-examples/userschema_registration.req | 3 + .../userschema_registration.resp | 69 +++++++++++++++++ src/plone/restapi/tests/test_documentation.py | 5 ++ .../restapi/tests/test_services_userschema.py | 74 ++++++++++++++++--- 7 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 news/1873.feature create mode 100644 src/plone/restapi/tests/http-examples/userschema_registration.req create mode 100644 src/plone/restapi/tests/http-examples/userschema_registration.resp diff --git a/docs/source/endpoints/userschema.md b/docs/source/endpoints/userschema.md index a59f582c92..a1e86121cd 100644 --- a/docs/source/endpoints/userschema.md +++ b/docs/source/endpoints/userschema.md @@ -9,26 +9,22 @@ myst: # User schema -```{note} - This is only available on Plone 5. -``` - Users in Plone have a set of properties defined by a default set of fields such as `fullname`, `email`, `portrait`, and so on. These properties define the site user's profile and the user itself via the Plone UI, or the site managers can add them in a variety of ways including PAS plugins. These fields are dynamic and customizable by integrators so they do not adhere to a fixed schema interface. -This dynamic schema is exposed by this endpoint in order to build the user's profile form. +This dynamic schema is exposed by this endpoint in order to build the user's profile form and the registration form. -## Getting the user schema +## Get the schema for the user profile -To get the current user schema, make a request to the `/@userschema` endpoint. +To get the current schema for the user profile, make a request to the `/@userschema` endpoint. ```{eval-rst} .. http:example:: curl httpie python-requests :request: ../../../src/plone/restapi/tests/http-examples/userschema.req ``` -The server will respond with the user schema. +The server will respond with the user profile schema. ```{literalinclude} ../../../src/plone/restapi/tests/http-examples/userschema.resp :language: http @@ -37,3 +33,22 @@ The server will respond with the user schema. The user schema uses the same serialization as the type's JSON schema. See {ref}`types-schema` for detailed documentation about the available field types. + +## Get the registration form + +In Plone you can configure each of the fields of the user schema to be available in only one of either the user profile form or registration form, or in both of them. + +To get the user schema available for the user registration form, make a request to the `@userschema/registration` endpoint. + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/userschema_registration.req +``` + +The server will respond with the user schema for registration. + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/userschema_registration.resp + :language: http +``` + +The user schema uses the same serialization as the type's JSON schema. diff --git a/news/1873.feature b/news/1873.feature new file mode 100644 index 0000000000..72a44ba38e --- /dev/null +++ b/news/1873.feature @@ -0,0 +1 @@ +Add a `@userschema/registration` endpoint to get the fields for the registration form. @erral diff --git a/src/plone/restapi/services/userschema/user.py b/src/plone/restapi/services/userschema/user.py index 17e201bcca..89688de421 100644 --- a/src/plone/restapi/services/userschema/user.py +++ b/src/plone/restapi/services/userschema/user.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from plone.app.users.browser.register import getRegisterSchema from plone.app.users.browser.userdatapanel import getUserDataSchema from plone.restapi.serializer.converters import json_compatible from plone.restapi.services import Service @@ -6,11 +7,23 @@ from plone.restapi.types.utils import get_fieldsets from plone.restapi.types.utils import get_jsonschema_properties from plone.restapi.types.utils import iter_fields +from zope.interface import implementer +from zope.publisher.interfaces import IPublishTraverse +@implementer(IPublishTraverse) class UserSchemaGet(Service): - def reply(self): - user_schema = getUserDataSchema() + def __init__(self, context, request): + super().__init__(context, request) + self.params = [] + + def publishTraverse(self, request, name): + # Consume any path segments after /@userschema as parameters + self.params.append(name) + return self + + def build_userschema_as_jsonschema(self, user_schema): + """function to build a jsonschema from user schema information""" fieldsets = get_fieldsets(self.context, self.request, user_schema) # Build JSON schema properties @@ -33,3 +46,17 @@ def reply(self): "required": required, "fieldsets": get_fieldset_infos(fieldsets), } + + def reply(self): + if len(self.params) == 0: + return self.build_userschema_as_jsonschema(getUserDataSchema()) + elif len(self.params) == 1 and self.params[0] == "registration": + return self.build_userschema_as_jsonschema(getRegisterSchema()) + + self.request.response.setStatus(400) + return dict( + error=dict( + type="Invalid parameters", + message="Parameters supplied are not valid.", + ) + ) diff --git a/src/plone/restapi/tests/http-examples/userschema_registration.req b/src/plone/restapi/tests/http-examples/userschema_registration.req new file mode 100644 index 0000000000..2a6eeaa0d8 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/userschema_registration.req @@ -0,0 +1,3 @@ +GET /plone/@userschema/registration HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/userschema_registration.resp b/src/plone/restapi/tests/http-examples/userschema_registration.resp new file mode 100644 index 0000000000..17970c2f7a --- /dev/null +++ b/src/plone/restapi/tests/http-examples/userschema_registration.resp @@ -0,0 +1,69 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "fieldsets": [ + { + "behavior": "plone", + "fields": [ + "fullname", + "email", + "username", + "password", + "password_ctl", + "mail_me" + ], + "id": "default", + "title": "Default" + } + ], + "properties": { + "email": { + "description": "We will use this address if you need to recover your password", + "factory": "Email", + "title": "Email", + "type": "string", + "widget": "email" + }, + "fullname": { + "description": "Enter full name, e.g. John Smith.", + "factory": "Text line (String)", + "title": "Full Name", + "type": "string" + }, + "mail_me": { + "default": false, + "description": "", + "factory": "Yes/No", + "title": "Send a confirmation mail with a link to set the password", + "type": "boolean" + }, + "password": { + "description": "Enter your new password.", + "factory": "Password", + "title": "Password", + "type": "string", + "widget": "password" + }, + "password_ctl": { + "description": "Re-enter the password. Make sure the passwords are identical.", + "factory": "Password", + "title": "Confirm password", + "type": "string", + "widget": "password" + }, + "username": { + "description": "Enter a user name, usually something like 'jsmith'. No spaces or special characters. Usernames and passwords are case sensitive, make sure the caps lock key is not enabled. This is the name used to log in.", + "factory": "Text line (String)", + "title": "User Name", + "type": "string" + } + }, + "required": [ + "email", + "username", + "password", + "password_ctl" + ], + "type": "object" +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 5f2c92d97d..c11c41e0cf 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -2516,6 +2516,11 @@ def test_documentation_schema_user(self): save_request_and_response_for_docs("userschema", response) + def test_documentation_schema_user_registration(self): + response = self.api_session.get("/@userschema/registration") + + save_request_and_response_for_docs("userschema_registration", response) + class TestRules(TestDocumentationBase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING diff --git a/src/plone/restapi/tests/test_services_userschema.py b/src/plone/restapi/tests/test_services_userschema.py index 5a09bb1e48..04257810c0 100644 --- a/src/plone/restapi/tests/test_services_userschema.py +++ b/src/plone/restapi/tests/test_services_userschema.py @@ -12,15 +12,6 @@ import unittest -try: - from Products.CMFPlone.factory import _IMREALLYPLONE5 # noqa -except ImportError: - PLONE5 = False -else: - PLONE5 = True - - -@unittest.skipIf(not PLONE5, "Just Plone 5 currently.") class TestUserSchemaEndpoint(unittest.TestCase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING @@ -63,8 +54,39 @@ def test_userschema_get(self): self.assertTrue("object", response["type"]) + def test_userschema_registration_get(self): + response = self.api_session.get("/@userschema/registration") + + self.assertEqual(200, response.status_code) + response = response.json() + + self.assertIn("fullname", response["fieldsets"][0]["fields"]) + self.assertIn("email", response["fieldsets"][0]["fields"]) + self.assertIn("password", response["fieldsets"][0]["fields"]) + self.assertIn("password_ctl", response["fieldsets"][0]["fields"]) + self.assertIn("username", response["fieldsets"][0]["fields"]) + self.assertIn("mail_me", response["fieldsets"][0]["fields"]) + + self.assertIn("fullname", response["properties"]) + self.assertIn("email", response["properties"]) + self.assertIn("password", response["properties"]) + self.assertIn("password_ctl", response["properties"]) + self.assertIn("username", response["properties"]) + self.assertIn("mail_me", response["properties"]) + + self.assertIn("email", response["required"]) + self.assertIn("username", response["required"]) + self.assertIn("password", response["required"]) + self.assertIn("password_ctl", response["required"]) + + self.assertTrue("object", response["type"]) + + def test_userschema_with_invalid_params(self): + response = self.api_session.get("/@userschema/something-invalid") + + self.assertEqual(400, response.status_code) + -@unittest.skipIf(not PLONE5, "Just Plone 5 currently.") class TestCustomUserSchema(unittest.TestCase): """test userschema endpoint with a custom defined schema. we have taken the same example as in plone.app.users, thatç @@ -133,7 +155,7 @@ def setUp(self): False Age - + False Department @@ -159,7 +181,7 @@ def setUp(self): False Pi - + False Vegetarian @@ -196,3 +218,31 @@ def test_userschema_get(self): self.assertIn("skills", response["fieldsets"][0]["fields"]) self.assertIn("pi", response["fieldsets"][0]["fields"]) self.assertIn("vegetarian", response["fieldsets"][0]["fields"]) + + def test_userschema_for_registration_get(self): + response = self.api_session.get("/@userschema/registration") + + self.assertEqual(200, response.status_code) + response = response.json() + # Default fields + self.assertIn("fullname", response["fieldsets"][0]["fields"]) + self.assertIn("email", response["fieldsets"][0]["fields"]) + self.assertIn("username", response["fieldsets"][0]["fields"]) + self.assertIn("password", response["fieldsets"][0]["fields"]) + self.assertIn("password_ctl", response["fieldsets"][0]["fields"]) + self.assertIn("mail_me", response["fieldsets"][0]["fields"]) + + # added fields + self.assertIn("department", response["fieldsets"][0]["fields"]) + self.assertIn("vegetarian", response["fieldsets"][0]["fields"]) + + # fields not shown in the regisration form + self.assertNotIn("home_page", response["fieldsets"][0]["fields"]) + self.assertNotIn("description", response["fieldsets"][0]["fields"]) + self.assertNotIn("location", response["fieldsets"][0]["fields"]) + self.assertNotIn("portrait", response["fieldsets"][0]["fields"]) + self.assertNotIn("birthdate", response["fieldsets"][0]["fields"]) + self.assertNotIn("another_date", response["fieldsets"][0]["fields"]) + self.assertNotIn("age", response["fieldsets"][0]["fields"]) + self.assertNotIn("skills", response["fieldsets"][0]["fields"]) + self.assertNotIn("pi", response["fieldsets"][0]["fields"])