diff --git a/README.rst b/README.rst index 4d6474d..9524b4f 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,31 @@ It includes a registry reader which reads operators, values and criteria from the Plone registry. +Filtering by the current item +----------------------------- + +The ``plone.app.querystring.operation.string.currentUID`` operation +("Current item") compares an index with the UID of the item where the query +runs, for example the page that holds a listing block. It needs no value from +the editor. + +No field uses this operation by default. It is meant for indexes that store +the UIDs of related items: a collection or listing placed on an item can then +show every item that points to it, such as "posts by this author". An add-on +enables it on the field for its own index in its ``registry.xml``:: + + + Authors + True + False + + plone.app.querystring.operation.string.currentUID + + Metadata + + + Compatibility with Plone versions --------------------------------- diff --git a/news/207.feature b/news/207.feature new file mode 100644 index 0000000..6ac3999 --- /dev/null +++ b/news/207.feature @@ -0,0 +1 @@ +Allow sorting query results by location (``path``), which orders items by their full physical path. @ericof diff --git a/news/208.feature b/news/208.feature new file mode 100644 index 0000000..5037edb --- /dev/null +++ b/news/208.feature @@ -0,0 +1 @@ +Add the ``plone.app.querystring.operation.string.currentUID`` ("Current item") operation, which filters an index by the UID of the item where the query runs. Add-ons can enable it on fields for their own relation indexes. @ericof diff --git a/src/plone/app/querystring/profiles.zcml b/src/plone/app/querystring/profiles.zcml index 9964d90..3e3c899 100644 --- a/src/plone/app/querystring/profiles.zcml +++ b/src/plone/app/querystring/profiles.zcml @@ -98,4 +98,12 @@ directory="profiles/upgrades/to_15" /> + + diff --git a/src/plone/app/querystring/profiles/default/metadata.xml b/src/plone/app/querystring/profiles/default/metadata.xml index c8b58bc..9a3e55b 100644 --- a/src/plone/app/querystring/profiles/default/metadata.xml +++ b/src/plone/app/querystring/profiles/default/metadata.xml @@ -1,6 +1,6 @@ - 15 + 16 profile-plone.app.registry:default diff --git a/src/plone/app/querystring/profiles/default/registry.xml b/src/plone/app/querystring/profiles/default/registry.xml index e151476..4ae3545 100644 --- a/src/plone/app/querystring/profiles/default/registry.xml +++ b/src/plone/app/querystring/profiles/default/registry.xml @@ -257,6 +257,19 @@ + + Current item + The item where the querystring results are displayed + plone.app.querystring.queryparser._currentUID + + + @@ -411,7 +424,7 @@ i18n:translate="" >The location of an item True - False + True plone.app.querystring.operation.string.absolutePath plone.app.querystring.operation.string.path diff --git a/src/plone/app/querystring/profiles/upgrades/to_16/registry.xml b/src/plone/app/querystring/profiles/upgrades/to_16/registry.xml new file mode 100644 index 0000000..b1f382a --- /dev/null +++ b/src/plone/app/querystring/profiles/upgrades/to_16/registry.xml @@ -0,0 +1,25 @@ + + + + + Current item + The item where the querystring results are displayed + plone.app.querystring.queryparser._currentUID + + + + + True + + + diff --git a/src/plone/app/querystring/queryparser.py b/src/plone/app/querystring/queryparser.py index 79086cd..4f56af7 100644 --- a/src/plone/app/querystring/queryparser.py +++ b/src/plone/app/querystring/queryparser.py @@ -251,6 +251,14 @@ def _currentUser(context, row): return {row.index: {"query": user.getId()}} +def _currentUID(context, row): + """Current item UID lookup""" + uid = IUUID(context, None) + if not uid: + return {} + return {row.index: {"query": uid}} + + def _showInactive(context, row): """Current user roles lookup in order to determine whether user should be allowed to view inactive content diff --git a/src/plone/app/querystring/tests/testQueryBuilder.py b/src/plone/app/querystring/tests/testQueryBuilder.py index ebe3efd..b4dcd74 100644 --- a/src/plone/app/querystring/tests/testQueryBuilder.py +++ b/src/plone/app/querystring/tests/testQueryBuilder.py @@ -1,3 +1,4 @@ +from plone.app.querystring.testing import PLONEAPPQUERYSTRING_INTEGRATION_TESTING from plone.app.querystring.testing import ( TEST_PROFILE_PLONEAPPQUERYSTRING_INTEGRATION_TESTING, ) @@ -403,3 +404,76 @@ def setUp(self): def testGettingJSONConfiguration(self): self.folder.restrictedTraverse("@@querybuilderjsonconfig")() + + +class TestQuerybuilderDefaultProfile(unittest.TestCase): + layer = PLONEAPPQUERYSTRING_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + self.request = TestRequest() + self.portal.invokeFactory("Folder", "listing", title="Listing") + listing = self.portal.listing + listing.invokeFactory("Folder", "section-b", title="Section B") + listing["section-b"].invokeFactory("Document", "page", title="Page") + listing.invokeFactory("Document", "about", title="About") + listing.invokeFactory("Folder", "section-a", title="Section A") + self.listing = listing + + def querybuilder(self, context): + return getMultiAdapter((context, self.request), name="querybuilderresults") + + def test_sort_on_path(self): + query = [ + { + "i": "path", + "o": "plone.app.querystring.operation.string.absolutePath", + "v": "/listing", + } + ] + results = self.querybuilder(self.portal)( + query=query, sort_on="path", brains=True + ) + self.assertEqual( + [brain.getPath() for brain in results], + [ + "/plone/listing", + "/plone/listing/about", + "/plone/listing/section-a", + "/plone/listing/section-b", + "/plone/listing/section-b/page", + ], + ) + + def test_sort_on_path_reverse(self): + query = [ + { + "i": "path", + "o": "plone.app.querystring.operation.string.absolutePath", + "v": "/listing/section-b", + } + ] + results = self.querybuilder(self.portal)( + query=query, sort_on="path", sort_order="reverse", brains=True + ) + self.assertEqual( + [brain.getPath() for brain in results], + ["/plone/listing/section-b/page", "/plone/listing/section-b"], + ) + + def test_current_uid(self): + page = self.listing["section-b"]["page"] + query = [ + { + "i": "UID", + "o": "plone.app.querystring.operation.string.currentUID", + } + ] + results = self.querybuilder(page)(query=query, brains=True) + self.assertEqual(len(results), 1) + self.assertEqual(results[0].UID, page.UID()) + + # The same query run on another context finds that context instead + results = self.querybuilder(self.listing)(query=query, brains=True) + self.assertEqual(len(results), 1) + self.assertEqual(results[0].UID, self.listing.UID()) diff --git a/src/plone/app/querystring/tests/testQueryParser.py b/src/plone/app/querystring/tests/testQueryParser.py index 2973fb4..8b36194 100644 --- a/src/plone/app/querystring/tests/testQueryParser.py +++ b/src/plone/app/querystring/tests/testQueryParser.py @@ -10,6 +10,8 @@ from plone.registry import Record from plone.registry import Registry from plone.registry.interfaces import IRegistry +from plone.uuid.interfaces import ATTRIBUTE_NAME +from plone.uuid.interfaces import IAttributeUUID from Products.CMFCore.interfaces import ICatalogTool from Products.CMFCore.interfaces import IMembershipTool from Products.CMFCore.interfaces import IURLTool @@ -104,6 +106,13 @@ class MockNavRoot(MockObject): pass +@implementer(IAttributeUUID) +class MockUUIDObject(MockObject): + def __init__(self, uid, path): + super().__init__(uid, path) + setattr(self, ATTRIBUTE_NAME, uid) + + class MockUser: def __init__(self, userid=None, roles=None): self.userid = userid or "Anonymous User" @@ -492,6 +501,19 @@ def test__currentUser(self): expected = {"Creator": {"query": "admin"}} self.assertEqual(parsed, expected) + def test__currentUID(self): + context = MockUUIDObject(uid="00000000000000001", path="/%s/foo" % MOCK_SITE_ID) + data = Row(index="related_to", operator="_currentUID", values=None) + parsed = queryparser._currentUID(context, data) + expected = {"related_to": {"query": "00000000000000001"}} + self.assertEqual(parsed, expected) + + def test__currentUID_without_uid(self): + context = MockObject(uid=None, path="/%s/foo" % MOCK_SITE_ID) + data = Row(index="related_to", operator="_currentUID", values=None) + parsed = queryparser._currentUID(context, data) + self.assertEqual(parsed, {}) + def test__showInactive(self): # Anonymous user u = MockUser() diff --git a/src/plone/app/querystring/tests/testRegistryIntegration.py b/src/plone/app/querystring/tests/testRegistryIntegration.py index 46ca75d..247456d 100644 --- a/src/plone/app/querystring/tests/testRegistryIntegration.py +++ b/src/plone/app/querystring/tests/testRegistryIntegration.py @@ -1,4 +1,5 @@ from plone.app.querystring.testing import PLONEAPPQUERYSTRING_INTEGRATION_TESTING +from plone.app.testing import applyProfile import unittest @@ -36,6 +37,19 @@ def test_date_lessthan(self): "plone.app.querystring.queryparser._dateLessThan", ) + def test_current_uid(self): + registry = self.portal.portal_registry + prefix = "plone.app.querystring.operation.string.currentUID" + + self.assertTrue(prefix + ".title" in registry) + + self.assertEqual(registry[prefix + ".title"], "Current item") + self.assertEqual( + registry[prefix + ".operation"], + "plone.app.querystring.queryparser._currentUID", + ) + self.assertIsNone(registry[prefix + ".widget"]) + class TestFieldDefinitions(unittest.TestCase): layer = PLONEAPPQUERYSTRING_INTEGRATION_TESTING @@ -80,3 +94,38 @@ def test_getobjpositioninparent_largerthan(self): # check if operation is used for getObjPositionInParent operations = registry.get(key) self.assertTrue(operation in operations) + + def test_path_sortable(self): + registry = self.portal.portal_registry + self.assertEqual(registry["plone.app.querystring.field.path.sortable"], True) + + +class TestUpgradeTo16(unittest.TestCase): + layer = PLONEAPPQUERYSTRING_INTEGRATION_TESTING + + def setUp(self): + self.portal = self.layer["portal"] + registry = self.portal.portal_registry + # Bring the registry back to its state before version 16 + prefix = "plone.app.querystring.operation.string.currentUID." + for key in [k for k in registry.records.keys() if k.startswith(prefix)]: + del registry.records[key] + registry["plone.app.querystring.field.path.sortable"] = False + + def test_upgrade(self): + registry = self.portal.portal_registry + prefix = "plone.app.querystring.operation.string.currentUID" + self.assertFalse(prefix + ".title" in registry) + + applyProfile(self.portal, "plone.app.querystring:upgrade_to_16") + + self.assertEqual(registry[prefix + ".title"], "Current item") + self.assertEqual( + registry[prefix + ".operation"], + "plone.app.querystring.queryparser._currentUID", + ) + field = "plone.app.querystring.field.path" + self.assertEqual(registry[field + ".sortable"], True) + # The other values of the path field are kept + self.assertEqual(registry[field + ".title"], "Location") + self.assertEqual(len(registry[field + ".operations"]), 3) diff --git a/src/plone/app/querystring/upgrades.zcml b/src/plone/app/querystring/upgrades.zcml index d044575..0ec33ae 100644 --- a/src/plone/app/querystring/upgrades.zcml +++ b/src/plone/app/querystring/upgrades.zcml @@ -170,4 +170,15 @@ /> + + + +