From 40a829a1d9a63b8b160b6267373cc1f641f556e5 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 16:00:42 +0200 Subject: [PATCH 01/10] Add querystring_search get method. --- .../services/querystringsearch/configure.zcml | 16 +++++++++++ .../restapi/services/querystringsearch/get.py | 27 ++++++++++++++++--- .../http-examples/querystringsearch_get.req | 3 +++ .../http-examples/querystringsearch_get.resp | 23 ++++++++++++++++ src/plone/restapi/tests/test_documentation.py | 9 +++++++ .../tests/test_services_querystringsearch.py | 12 +++++++++ 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 src/plone/restapi/tests/http-examples/querystringsearch_get.req create mode 100644 src/plone/restapi/tests/http-examples/querystringsearch_get.resp diff --git a/src/plone/restapi/services/querystringsearch/configure.zcml b/src/plone/restapi/services/querystringsearch/configure.zcml index 9a7ad20595..899f1f1156 100644 --- a/src/plone/restapi/services/querystringsearch/configure.zcml +++ b/src/plone/restapi/services/querystringsearch/configure.zcml @@ -18,4 +18,20 @@ permission="zope2.View" name="@querystring-search" /> + + + + diff --git a/src/plone/restapi/services/querystringsearch/get.py b/src/plone/restapi/services/querystringsearch/get.py index 29ed3f9471..b44d5a3966 100644 --- a/src/plone/restapi/services/querystringsearch/get.py +++ b/src/plone/restapi/services/querystringsearch/get.py @@ -6,6 +6,8 @@ from plone.restapi.services import Service from zope.component import getMultiAdapter +import json + zcatalog_version = get_distribution("Products.ZCatalog").version if parse_version(zcatalog_version) >= parse_version("5.1"): @@ -14,11 +16,14 @@ SUPPORT_NOT_UUID_QUERIES = False -class QuerystringSearchPost(Service): +class QuerystringSearch: """Returns the querystring search results given a p.a.querystring data.""" - def reply(self): - data = json_body(self.request) + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self, data): query = data.get("query", None) b_start = int(data.get("b_start", 0)) b_size = int(data.get("b_size", 25)) @@ -60,3 +65,19 @@ def reply(self): fullobjects=fullobjects ) return results + + +class QuerystringSearchPost(Service): + """Returns the querystring search results given a p.a.querystring data.""" + + def reply(self): + querystring_search = QuerystringSearch(self.context, self.request) + return querystring_search(data=json_body(self.request)) + + +class QuerystringSearchGet(Service): + """Returns the querystring search results given a p.a.querystring data.""" + + def reply(self): + querystring_search = QuerystringSearch(self.context, self.request) + return querystring_search(data=json.loads(self.request.form.get("query", "{}"))) diff --git a/src/plone/restapi/tests/http-examples/querystringsearch_get.req b/src/plone/restapi/tests/http-examples/querystringsearch_get.req new file mode 100644 index 0000000000..a5b2dba4e9 --- /dev/null +++ b/src/plone/restapi/tests/http-examples/querystringsearch_get.req @@ -0,0 +1,3 @@ +GET /plone/@querystring-search?query=%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D HTTP/1.1 +Accept: application/json +Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/http-examples/querystringsearch_get.resp b/src/plone/restapi/tests/http-examples/querystringsearch_get.resp new file mode 100644 index 0000000000..082bc772ae --- /dev/null +++ b/src/plone/restapi/tests/http-examples/querystringsearch_get.resp @@ -0,0 +1,23 @@ +HTTP/1.1 200 OK +Content-Type: application/json + +{ + "@id": "http://localhost:55001/plone/@querystring-search", + "items": [ + { + "@id": "http://localhost:55001/plone/front-page", + "@type": "Document", + "description": "Congratulations! You have successfully installed Plone.", + "review_state": "private", + "title": "Welcome to Plone" + }, + { + "@id": "http://localhost:55001/plone/testdocument", + "@type": "Document", + "description": "", + "review_state": "private", + "title": "Test Document" + } + ], + "items_total": 2 +} diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 6e8b88ded7..4bc7e6a674 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -1686,6 +1686,15 @@ def test_querystringsearch_post(self): ) save_request_and_response_for_docs("querystringsearch_post", response) + def test_querystringsearch_get(self): + url = "/@querystring-search?query=%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D" + + self.portal.invokeFactory("Document", "testdocument", title="Test Document") + transaction.commit() + + response = self.api_session.get(url) + save_request_and_response_for_docs("querystringsearch_get", response) + def test_system_get(self): response = self.api_session.get("/@system") save_request_for_docs("system_get", response) diff --git a/src/plone/restapi/tests/test_services_querystringsearch.py b/src/plone/restapi/tests/test_services_querystringsearch.py index 8dd77f7dc6..69f777a3bd 100644 --- a/src/plone/restapi/tests/test_services_querystringsearch.py +++ b/src/plone/restapi/tests/test_services_querystringsearch.py @@ -52,6 +52,18 @@ def test_querystringsearch_basic(self): self.assertEqual(len(response.json()["items"]), 1) self.assertNotIn("effective", response.json()["items"][0]) + def test_querystringsearch_basic_get(self): + response = self.api_session.get( + "/@querystring-search?query=%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D" + ) + + self.assertEqual(response.status_code, 200) + self.assertIn("items", response.json()) + self.assertIn("items_total", response.json()) + self.assertEqual(response.json()["items_total"], 1) + self.assertEqual(len(response.json()["items"]), 1) + self.assertNotIn("effective", response.json()["items"][0]) + def test_querystringsearch_fullobjects(self): response = self.api_session.post( "/@querystring-search", From d8b6c9d713ce574541997432d5c0dd4a453d63e6 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 16:03:56 +0200 Subject: [PATCH 02/10] Add changelog. --- news/1616.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/1616.feature diff --git a/news/1616.feature b/news/1616.feature new file mode 100644 index 0000000000..be2a4c579f --- /dev/null +++ b/news/1616.feature @@ -0,0 +1 @@ +Add querystring_search get method. @robgietema From 326e37b389e897d085e24f346e91761062ad2b84 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 16:34:10 +0200 Subject: [PATCH 03/10] Fix doc generation. --- src/plone/restapi/tests/test_documentation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index 4bc7e6a674..e6f0b164d6 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -1687,12 +1687,13 @@ def test_querystringsearch_post(self): save_request_and_response_for_docs("querystringsearch_post", response) def test_querystringsearch_get(self): - url = "/@querystring-search?query=%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D" + query = {"query": "%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D"} + url = "/@querystring-search" self.portal.invokeFactory("Document", "testdocument", title="Test Document") transaction.commit() - response = self.api_session.get(url) + response = self.api_session.get(url, params=query) save_request_and_response_for_docs("querystringsearch_get", response) def test_system_get(self): From 42e8f0bbe67f59b9fe63bf8f93d7a43f0d327413 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 16:47:07 +0200 Subject: [PATCH 04/10] Fix doc tests. --- src/plone/restapi/services/querystringsearch/get.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/plone/restapi/services/querystringsearch/get.py b/src/plone/restapi/services/querystringsearch/get.py index b44d5a3966..79de69accd 100644 --- a/src/plone/restapi/services/querystringsearch/get.py +++ b/src/plone/restapi/services/querystringsearch/get.py @@ -4,6 +4,7 @@ from plone.restapi.deserializer import json_body from plone.restapi.interfaces import ISerializeToJson from plone.restapi.services import Service +from urllib import parse from zope.component import getMultiAdapter import json @@ -80,4 +81,6 @@ class QuerystringSearchGet(Service): def reply(self): querystring_search = QuerystringSearch(self.context, self.request) - return querystring_search(data=json.loads(self.request.form.get("query", "{}"))) + return querystring_search( + data=json.loads(parse.unquote(self.request.form.get("query", "{}"))) + ) From ae97a2b09d47c0e1057cf516f01fb4f9f59dde13 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 17:04:28 +0200 Subject: [PATCH 05/10] Fix doc test response. --- .../restapi/tests/http-examples/querystringsearch_get.resp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plone/restapi/tests/http-examples/querystringsearch_get.resp b/src/plone/restapi/tests/http-examples/querystringsearch_get.resp index 082bc772ae..780f8f4ee7 100644 --- a/src/plone/restapi/tests/http-examples/querystringsearch_get.resp +++ b/src/plone/restapi/tests/http-examples/querystringsearch_get.resp @@ -2,7 +2,7 @@ HTTP/1.1 200 OK Content-Type: application/json { - "@id": "http://localhost:55001/plone/@querystring-search", + "@id": "http://localhost:55001/plone/@querystring-search?query=%257B%2522query%2522%253A%255B%257B%2522i%2522%253A%2522portal_type%2522%252C%2522o%2522%253A%2520%2522plone.app.querystring.operation.selection.any%2522%252C%2522v%2522%253A%255B%2522Document%2522%255D%257D%255D%257D", "items": [ { "@id": "http://localhost:55001/plone/front-page", From 40648f6d69d5856273bd268e2f2ab7a505f28ab0 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Tue, 4 Apr 2023 17:18:02 +0200 Subject: [PATCH 06/10] Fix black. --- .../restapi/tests/http-examples/querystringsearch_get.req | 2 +- src/plone/restapi/tests/test_documentation.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plone/restapi/tests/http-examples/querystringsearch_get.req b/src/plone/restapi/tests/http-examples/querystringsearch_get.req index a5b2dba4e9..0f859a5cc5 100644 --- a/src/plone/restapi/tests/http-examples/querystringsearch_get.req +++ b/src/plone/restapi/tests/http-examples/querystringsearch_get.req @@ -1,3 +1,3 @@ -GET /plone/@querystring-search?query=%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D HTTP/1.1 +GET /plone/@querystring-search?query=%257B%2522query%2522%253A%255B%257B%2522i%2522%253A%2522portal_type%2522%252C%2522o%2522%253A%2520%2522plone.app.querystring.operation.selection.any%2522%252C%2522v%2522%253A%255B%2522Document%2522%255D%257D%255D%257D HTTP/1.1 Accept: application/json Authorization: Basic YWRtaW46c2VjcmV0 diff --git a/src/plone/restapi/tests/test_documentation.py b/src/plone/restapi/tests/test_documentation.py index e6f0b164d6..2103b1e2e3 100644 --- a/src/plone/restapi/tests/test_documentation.py +++ b/src/plone/restapi/tests/test_documentation.py @@ -1687,7 +1687,9 @@ def test_querystringsearch_post(self): save_request_and_response_for_docs("querystringsearch_post", response) def test_querystringsearch_get(self): - query = {"query": "%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D"} + query = { + "query": "%7B%22query%22%3A%5B%7B%22i%22%3A%22portal_type%22%2C%22o%22%3A%20%22plone.app.querystring.operation.selection.any%22%2C%22v%22%3A%5B%22Document%22%5D%7D%5D%7D" + } url = "/@querystring-search" self.portal.invokeFactory("Document", "testdocument", title="Test Document") From 74972212c61bbb54c08c8440646c834ea1798196 Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Wed, 5 Apr 2023 16:30:31 +0200 Subject: [PATCH 07/10] Add docs. --- docs/source/endpoints/querystringsearch.md | 49 ++++++++-------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/docs/source/endpoints/querystringsearch.md b/docs/source/endpoints/querystringsearch.md index 953348f4ec..00000285bf 100644 --- a/docs/source/endpoints/querystringsearch.md +++ b/docs/source/endpoints/querystringsearch.md @@ -1,17 +1,17 @@ --- myst: html_meta: - "description": "The @querystring-search endpoint returns search results that can be filtered on search criteria." - "property=og:description": "The @querystring-search endpoint returns search results that can be filtered on search criteria." - "property=og:title": "Querystring Search" - "keywords": "Plone, plone.restapi, REST, API, Querystring, Search" + 'description': 'The @querystring-search endpoint returns search results that can be filtered on search criteria.' + 'property=og:description': 'The @querystring-search endpoint returns search results that can be filtered on search criteria.' + 'property=og:title': 'Querystring Search' + 'keywords': 'Plone, plone.restapi, REST, API, Querystring, Search' --- # Querystring Search The `@querystring-search` endpoint returns search results that can be filtered on search criteria. -Call the `/@querystring-search` endpoint with a `POST` request and a query in the request body: +Call the `/@querystring-search` endpoint with a `POST` or a `GET`. When using the `POST` request you provide a query in the request body: ```{eval-rst} .. http:example:: curl httpie python-requests @@ -24,6 +24,19 @@ The server will respond with the results that are filtered based on the query yo :language: http ``` +When using the `GET` request you provide the query as a json urlencoded string as a parameter. + +```{eval-rst} +.. http:example:: curl httpie python-requests + :request: ../../../src/plone/restapi/tests/http-examples/querystringsearch_get.req +``` + +The server will respond with the results that are filtered based on the query you provided: + +```{literalinclude} ../../../src/plone/restapi/tests/http-examples/querystringsearch_get.resp +:language: http +``` + Parameters the endpoint will accept: - `query` - `plone.app.querystring` query, required @@ -34,10 +47,8 @@ Parameters the endpoint will accept: - `limit` - integer, limits the number of returned results - `fullobjects` - boolean, if `true` then return the full objects instead of just the summary serialization - ## Parameters - ### Batch Start (`b_start`) The `b_start` parameter defines the first item of the batch: @@ -58,7 +69,6 @@ The `b_start` parameter defines the first item of the batch: The `b_size` parameter is optional. The default value is `0`. - ### Batch Size (b_size) The `b_size` parameter defines the number of elements a single batch returns: @@ -79,7 +89,6 @@ The `b_size` parameter defines the number of elements a single batch returns: The parameter is optional. The default value is `25`. - ### Sort on The `sort_on` parameter defines the field that is used to sort the returned search results: @@ -100,7 +109,6 @@ The `sort_on` parameter defines the field that is used to sort the returned sear The `sort_on` parameter is optional. The default value is `None`. - ### Sort Order The `sort_order` parameter defines the sort order when the `sort_on` field has been set: @@ -126,7 +134,6 @@ The sort_order can be either `ascending` or `descending`. `ascending` means from A to Z for a text field. `reverse` is an alias equivalent to `descending`. - ### Limit Querystring `query` with a `limit` parameter: @@ -147,7 +154,6 @@ Querystring `query` with a `limit` parameter: The `limit` parameter is optional. The default value is `1000`. - ### Query The `query` parameter is a list that contains an arbitrary number of `filters`: @@ -176,10 +182,8 @@ The following types of filters are available: - Date filters - Text Filters - #### Metadata Filters - ##### Creator The `creator` of the content object. @@ -212,7 +216,6 @@ You can either set the currently logged in user: } ``` - ##### Shortname `Shortname` is the ID of the object that is shown as the last part of the URL: @@ -229,7 +232,6 @@ You can either set the currently logged in user: } ``` - ##### Location `Location` is the path of the content object on the site. @@ -305,7 +307,6 @@ The path can contain a depth parameter that is separated with `::`: } ``` - ##### Type Filter by portal type: @@ -322,7 +323,6 @@ Filter by portal type: } ``` - ##### Review State Filter results by review state: @@ -339,7 +339,6 @@ Filter results by review state: } ``` - ##### Show Inactive Show inactive will return content objects that is expired for a given role: @@ -356,10 +355,8 @@ Show inactive will return content objects that is expired for a given role: } ``` - #### Text Filters - ##### Description Filter content that contains a term in the Description field: @@ -376,7 +373,6 @@ Filter content that contains a term in the Description field: } ``` - ##### Searchable Text Filter content that contains a term in the SearchableText (all searchable fields in the catalog): @@ -393,7 +389,6 @@ Filter content that contains a term in the SearchableText (all searchable fields } ``` - ##### Tag Filter by a tag (subjects field): @@ -410,7 +405,6 @@ Filter by a tag (subjects field): } ``` - ##### Title Filter by exact Title match: @@ -425,10 +419,8 @@ Filter by exact Title match: ] ``` - #### Date Filters - ##### Creation Date Filter by creation date: @@ -445,7 +437,6 @@ Filter by creation date: } ``` - ##### Effective Date Filter by effective date: @@ -463,7 +454,6 @@ Filter by effective date: } ``` - ##### Event end date Filter by event end date: @@ -480,7 +470,6 @@ Filter by event end date: } ``` - ##### Event start date Filter by event start date: @@ -497,7 +486,6 @@ Filter by event start date: } ``` - ##### Expiration date Filter by expiration date: @@ -515,7 +503,6 @@ Filter by expiration date: } ``` - ##### Modification date Filter by modification date: From 4f1d2314c2ce5ec3d8cce6599b79ec98b2c21b76 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 6 Apr 2023 08:36:43 +0200 Subject: [PATCH 08/10] Update docs/source/endpoints/querystringsearch.md Co-authored-by: Steve Piercy --- docs/source/endpoints/querystringsearch.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/endpoints/querystringsearch.md b/docs/source/endpoints/querystringsearch.md index 00000285bf..5a62f21ef0 100644 --- a/docs/source/endpoints/querystringsearch.md +++ b/docs/source/endpoints/querystringsearch.md @@ -24,7 +24,7 @@ The server will respond with the results that are filtered based on the query yo :language: http ``` -When using the `GET` request you provide the query as a json urlencoded string as a parameter. +When using the `GET` request, provide the query as a JSON URL-encoded string as a parameter. ```{eval-rst} .. http:example:: curl httpie python-requests From 4bae04769d35cc4e09a580ced57bfd46aaab62f2 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 6 Apr 2023 08:37:45 +0200 Subject: [PATCH 09/10] Apply suggestions from code review Co-authored-by: Steve Piercy --- docs/source/endpoints/querystringsearch.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/endpoints/querystringsearch.md b/docs/source/endpoints/querystringsearch.md index 5a62f21ef0..54b7c35304 100644 --- a/docs/source/endpoints/querystringsearch.md +++ b/docs/source/endpoints/querystringsearch.md @@ -11,7 +11,9 @@ myst: The `@querystring-search` endpoint returns search results that can be filtered on search criteria. -Call the `/@querystring-search` endpoint with a `POST` or a `GET`. When using the `POST` request you provide a query in the request body: +Call the `/@querystring-search` endpoint with either a `POST` or `GET` request. + +When using the `POST` request, provide a query in the request body: ```{eval-rst} .. http:example:: curl httpie python-requests From 6b3cefd162ca76631c0d6077fb6ea48eda9f16cb Mon Sep 17 00:00:00 2001 From: Rob Gietema Date: Thu, 6 Apr 2023 08:45:12 +0200 Subject: [PATCH 10/10] Fix quotes. --- docs/source/endpoints/querystringsearch.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/endpoints/querystringsearch.md b/docs/source/endpoints/querystringsearch.md index 54b7c35304..7cc5666636 100644 --- a/docs/source/endpoints/querystringsearch.md +++ b/docs/source/endpoints/querystringsearch.md @@ -1,10 +1,10 @@ --- myst: html_meta: - 'description': 'The @querystring-search endpoint returns search results that can be filtered on search criteria.' - 'property=og:description': 'The @querystring-search endpoint returns search results that can be filtered on search criteria.' - 'property=og:title': 'Querystring Search' - 'keywords': 'Plone, plone.restapi, REST, API, Querystring, Search' + "description": "The @querystring-search endpoint returns search results that can be filtered on search criteria." + "property=og:description": "The @querystring-search endpoint returns search results that can be filtered on search criteria." + "property=og:title": "Querystring Search" + "keywords": "Plone, plone.restapi, REST, API, Querystring, Search" --- # Querystring Search