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 @@
/>
+
+
+
+