From 5f306f41d4d20950bf2c785efba0c7881bd277f9 Mon Sep 17 00:00:00 2001 From: Syed Ahmed Mubasiruddin <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:04:21 +0530 Subject: [PATCH 01/11] Add sort_on=title parameter to @vocabularies with tests and docs --- docs/source/endpoints/vocabularies.md | 8 +++++++ src/plone/restapi/serializer/vocabularies.py | 13 ++++++++++++ .../tests/test_services_vocabularies.py | 21 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/docs/source/endpoints/vocabularies.md b/docs/source/endpoints/vocabularies.md index 7d6fcda3b9..a2199db21a 100644 --- a/docs/source/endpoints/vocabularies.md +++ b/docs/source/endpoints/vocabularies.md @@ -159,6 +159,14 @@ Use the `tokens` parameter to filter vocabulary terms by a list of tokens: ```{literalinclude} ../../../src/plone/restapi/tests/http-examples/vocabularies_get_filtered_by_token_list.resp :language: http ``` +## Sorting + +```{eval-rst} +.. http:get:: (context)/@vocabularies/(vocab_name)?sort_on=title +``` + +Vocabulary terms can be sorted by title using the ``sort_on=title`` parameter. +Sorting is applied server-side before results are batched. ## Get a source diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index 2235ced8a5..06023a771a 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -63,6 +63,19 @@ def __call__(self, vocabulary_id): continue terms.append(term) + # <--- The loop ends here. Sorting starts here. + # Optional sorting by title (before batching) + sort_on = self.request.form.get("sort_on") + if sort_on == "title": + terms.sort( + key=lambda term: ( + translate( + safe_text(getattr(term, "title", None) or ""), + context=self.request, + ) + or "" + ).lower() + ) serialized_terms = [] # Do not batch parameter is set diff --git a/src/plone/restapi/tests/test_services_vocabularies.py b/src/plone/restapi/tests/test_services_vocabularies.py index cb0774c3fe..e60cb73e97 100644 --- a/src/plone/restapi/tests/test_services_vocabularies.py +++ b/src/plone/restapi/tests/test_services_vocabularies.py @@ -468,6 +468,27 @@ def test_big_vocabulary_not_batched(self): response = response.json() self.assertEqual(len(response["items"]), 100) + def test_get_vocabulary_sorted_by_title(self): + response = self.api_session.get( + "/@vocabularies/plone.restapi.tests.test_vocabulary?sort_on=title&b_size=-1" + ) + + self.assertEqual(200, response.status_code) + response = response.json() + + self.assertEqual( + [item["title"] for item in response["items"]], + [ + "This is a title for the seventh term", + "Title 1", + "Title 2", + "token3", + "token4", + "Tötle 5", + "Tötle 6", + ], + ) + def tearDown(self): self.api_session.close() gsm = getGlobalSiteManager() From 5dd0ee02f883b06e47baa2da166b97655ea0a80f Mon Sep 17 00:00:00 2001 From: Syed Ahmed Mubasiruddin <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:12:45 +0530 Subject: [PATCH 02/11] Add news fragment for sort_on=title feature --- news/1990.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1990.feature diff --git a/news/1990.feature b/news/1990.feature new file mode 100644 index 0000000000..85068f8940 --- /dev/null +++ b/news/1990.feature @@ -0,0 +1 @@ +Add optional ``sort_on=title`` parameter to ``@vocabularies`` endpoint. \ No newline at end of file From 205ea7af350b8d04feb797593dc03bdaa728e4a0 Mon Sep 17 00:00:00 2001 From: Syed Ahmed Mubasiruddin <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:05:35 +0000 Subject: [PATCH 03/11] Align vocabulary sorting with serialized output and fix test order --- src/plone/restapi/serializer/vocabularies.py | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index 06023a771a..235f8a5b81 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -66,16 +66,17 @@ def __call__(self, vocabulary_id): # <--- The loop ends here. Sorting starts here. # Optional sorting by title (before batching) sort_on = self.request.form.get("sort_on") - if sort_on == "title": - terms.sort( - key=lambda term: ( - translate( - safe_text(getattr(term, "title", None) or ""), - context=self.request, - ) - or "" - ).lower() - ) + if vocabulary_id and sort_on == "title": + + def sort_key(term): + title = ( + term.title if ITitledTokenizedTerm.providedBy(term) else term.token + ) + if isinstance(title, bytes): + title = title.decode("UTF-8") + return translate(safe_text(title), context=self.request).lower() + + terms.sort(key=sort_key) serialized_terms = [] # Do not batch parameter is set From 2973f75cb7e5c1309b5191ea9eb7ea9316636c9d Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:13:36 +0530 Subject: [PATCH 04/11] Update news/1990.feature Co-authored-by: Steve Piercy --- news/1990.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/1990.feature b/news/1990.feature index 85068f8940..26d19725a3 100644 --- a/news/1990.feature +++ b/news/1990.feature @@ -1 +1 @@ -Add optional ``sort_on=title`` parameter to ``@vocabularies`` endpoint. \ No newline at end of file +Added support for sorting vocabularies by title before batching for the `@vocabularies` endpoint. @hasansyed107 \ No newline at end of file From 2aba871db7ad5eda271d5cf11ed0ca636723fadf Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 20:13:52 +0530 Subject: [PATCH 05/11] Update docs/source/endpoints/vocabularies.md Co-authored-by: Steve Piercy --- docs/source/endpoints/vocabularies.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/endpoints/vocabularies.md b/docs/source/endpoints/vocabularies.md index a2199db21a..6d506f8b85 100644 --- a/docs/source/endpoints/vocabularies.md +++ b/docs/source/endpoints/vocabularies.md @@ -159,15 +159,14 @@ Use the `tokens` parameter to filter vocabulary terms by a list of tokens: ```{literalinclude} ../../../src/plone/restapi/tests/http-examples/vocabularies_get_filtered_by_token_list.resp :language: http ``` -## Sorting -```{eval-rst} -.. http:get:: (context)/@vocabularies/(vocab_name)?sort_on=title -``` +### Sort vocabularies by title -Vocabulary terms can be sorted by title using the ``sort_on=title`` parameter. +Sort vocabulary terms by title using the `sort_on=title` parameter. Sorting is applied server-side before results are batched. +INSERT_MYST_MARKUP_FOLLOWING_OTHER_EXAMPLES_HERE + ## Get a source ```{eval-rst} From 06341d40455c4f270dbb534e57959c4d662e8cdf Mon Sep 17 00:00:00 2001 From: Syed Ahmed Mubasiruddin <136695107+hasansyed107@users.noreply.github.com> Date: Sat, 21 Feb 2026 04:05:35 +0000 Subject: [PATCH 06/11] Align vocabulary sorting with serialized output and fix test order --- docs/source/endpoints/vocabularies.md | 12 +++- .../vocabularies_get_sorted_by_title.req | 4 ++ .../vocabularies_get_sorted_by_title.resp | 57 +++++++++++++++++++ src/plone/restapi/tests/test_documentation.py | 6 ++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req create mode 100644 src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.resp diff --git a/docs/source/endpoints/vocabularies.md b/docs/source/endpoints/vocabularies.md index 6d506f8b85..41ea71513e 100644 --- a/docs/source/endpoints/vocabularies.md +++ b/docs/source/endpoints/vocabularies.md @@ -165,8 +165,18 @@ Use the `tokens` parameter to filter vocabulary terms by a list of tokens: Sort vocabulary terms by title using the `sort_on=title` parameter. Sorting is applied server-side before results are batched. -INSERT_MYST_MARKUP_FOLLOWING_OTHER_EXAMPLES_HERE +```{eval-rst} +.. http:get:: (context)/@vocabularies/(vocab_name)?sort_on=title +``` + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req +``` +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.resp +:language: http +``` ## Get a source ```{eval-rst} diff --git a/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req b/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req new file mode 100644 index 0000000000..69644a72e0 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req @@ -0,0 +1,4 @@ +GET /plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?sort_on=title HTTP/1.1 +Accept: application/json +Accept-Language: es +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.resp b/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.resp new file mode 100644 index 0000000000..15dd37b920 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.resp @@ -0,0 +1,57 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:55001/plone/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes", + "items": [ + { + "title": "Archivo", + "token": "File" + }, + { + "title": "Carpeta", + "token": "Folder" + }, + { + "title": "Colecci\u00f3n", + "token": "Collection" + }, + { + "title": "Comentarios", + "token": "Discussion Item" + }, + { + "title": "DX Test Document", + "token": "DXTestDocument" + }, + { + "title": "Enlace", + "token": "Link" + }, + { + "title": "Evento", + "token": "Event" + }, + { + "title": "Imagen", + "token": "Image" + }, + { + "title": "Noticia", + "token": "News Item" + }, + { + "title": "P\u00e1gina", + "token": "Document" + }, + { + "title": "Test Document", + "token": "ATTestDocument" + }, + { + "title": "Test Folder", + "token": "ATTestFolder" + } + ], + "items_total": 12 +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 67b3620ba8..a5c23c4dc4 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -1911,6 +1911,12 @@ def test_translate_messages_addons(self): response = self.api_session.get("/@addons") save_request_and_response_for_docs("translated_messages_addons", response) + def test_documentation_vocabularies_get_sorted_by_title(self): + response = self.api_session.get( + "/@vocabularies/plone.app.vocabularies.ReallyUserFriendlyTypes?sort_on=title" + ) + save_request_and_response_for_docs("vocabularies_get_sorted_by_title", response) + class TestCommenting(TestDocumentationBase): layer = PLONE_RESTAPI_DX_FUNCTIONAL_TESTING From 6781eba8b00cf794ec758aa6ae0b593ee5bf3bc1 Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Sun, 22 Feb 2026 07:04:45 +0530 Subject: [PATCH 07/11] Update docs/source/endpoints/vocabularies.md Co-authored-by: Steve Piercy --- docs/source/endpoints/vocabularies.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/source/endpoints/vocabularies.md b/docs/source/endpoints/vocabularies.md index 41ea71513e..63b9f9b15d 100644 --- a/docs/source/endpoints/vocabularies.md +++ b/docs/source/endpoints/vocabularies.md @@ -164,11 +164,6 @@ Use the `tokens` parameter to filter vocabulary terms by a list of tokens: Sort vocabulary terms by title using the `sort_on=title` parameter. Sorting is applied server-side before results are batched. - -```{eval-rst} -.. http:get:: (context)/@vocabularies/(vocab_name)?sort_on=title -``` - ```{eval-rst} .. http:example:: curl httpie python-requests :request: ../../../src/plone/restapi/tests/http-examples/vocabularies_get_sorted_by_title.req From a9deb0010095aec63582f07ffd29a0057539a61e Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:52:16 +0530 Subject: [PATCH 08/11] Update src/plone/restapi/serializer/vocabularies.py Co-authored-by: David Glick --- src/plone/restapi/serializer/vocabularies.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index 235f8a5b81..c857d446e8 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -63,7 +63,6 @@ def __call__(self, vocabulary_id): continue terms.append(term) - # <--- The loop ends here. Sorting starts here. # Optional sorting by title (before batching) sort_on = self.request.form.get("sort_on") if vocabulary_id and sort_on == "title": From 57e723e1f886d80bba1f5d3cddf9d595db3d5e39 Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:52:31 +0530 Subject: [PATCH 09/11] Update src/plone/restapi/serializer/vocabularies.py Co-authored-by: David Glick --- src/plone/restapi/serializer/vocabularies.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index c857d446e8..afc786bce4 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -68,9 +68,7 @@ def __call__(self, vocabulary_id): if vocabulary_id and sort_on == "title": def sort_key(term): - title = ( - term.title if ITitledTokenizedTerm.providedBy(term) else term.token - ) + title = getattr(term, "title", term.token) if isinstance(title, bytes): title = title.decode("UTF-8") return translate(safe_text(title), context=self.request).lower() From de8e0ba44c1e75b06fbc53a66987ac5c5aeabaea Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:59:16 +0530 Subject: [PATCH 10/11] Final cleanup per review comments --- src/plone/restapi/serializer/vocabularies.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index afc786bce4..749f10283b 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -69,9 +69,7 @@ def __call__(self, vocabulary_id): def sort_key(term): title = getattr(term, "title", term.token) - if isinstance(title, bytes): - title = title.decode("UTF-8") - return translate(safe_text(title), context=self.request).lower() + return translate(title, context=self.request).lower() terms.sort(key=sort_key) serialized_terms = [] From 61fb2acb13e808909ed29098e23137caa6991a66 Mon Sep 17 00:00:00 2001 From: hasansyed107 <136695107+hasansyed107@users.noreply.github.com> Date: Tue, 24 Feb 2026 07:04:24 +0530 Subject: [PATCH 11/11] Update src/plone/restapi/serializer/vocabularies.py Co-authored-by: David Glick --- src/plone/restapi/serializer/vocabularies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/serializer/vocabularies.py b/src/plone/restapi/serializer/vocabularies.py index 749f10283b..1487151304 100644 --- a/src/plone/restapi/serializer/vocabularies.py +++ b/src/plone/restapi/serializer/vocabularies.py @@ -68,7 +68,7 @@ def __call__(self, vocabulary_id): if vocabulary_id and sort_on == "title": def sort_key(term): - title = getattr(term, "title", term.token) + title = getattr(term, "title", None) or term.token return translate(title, context=self.request).lower() terms.sort(key=sort_key)