From 18da4ec79546c4c44a60dd237aea700f0c2ba9d7 Mon Sep 17 00:00:00 2001 From: Dylan Jay Date: Tue, 21 Jan 2025 12:30:22 +0700 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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, + )