diff --git a/last_commit.txt b/last_commit.txt index fe335c605e..763ee6c25c 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -2,26 +2,55 @@ Repository: plone.app.querystring Branch: refs/heads/master -Date: 2022-05-23T22:17:46+02:00 -Author: Andrea Cecchi (cekk) -Commit: https://github.com/plone/plone.app.querystring/commit/08376be5e6a135f1e05c5ae16dc7f7aef93f91aa - -Fix query update with custom query (#103) - -* Fix query update with custom query - -* add changelog - -* fix check - -* simpler code and one more test to verify - -Co-authored-by: Peter Mathis <peter.mathis@kombinat.at> +Date: 2022-05-24T08:21:25+02:00 +Author: Johannes Raggam (thet) +Commit: https://github.com/plone/plone.app.querystring/commit/6b2b8e5edc9c7836daed78f612cf5752b5065b1b + +Add negation-query operators string.isNot and selection.none. + +New ``plone.app.querystring.operation.string.isNot`` and ``plone.app.querystring.operation.selection.none`` including upgrade steps. + +Files changed: +A news/110.feature +A plone/app/querystring/profiles/upgrades/to_14/registry.xml +M plone/app/querystring/hiddenprofiles.py +M plone/app/querystring/indexmodifiers/query_index_modifiers.py +M plone/app/querystring/profiles.zcml +M plone/app/querystring/profiles/default/metadata.xml +M plone/app/querystring/profiles/default/registry.xml +M plone/app/querystring/queryparser.py +M plone/app/querystring/tests/testIndexmodifiers.py +M plone/app/querystring/tests/testQueryBuilder.py +M plone/app/querystring/tests/testRegistryIntegration.py +M plone/app/querystring/upgrades.zcml + +b'diff --git a/news/110.feature b/news/110.feature\nnew file mode 100644\nindex 0000000..d9709b7\n--- /dev/null\n+++ b/news/110.feature\n@@ -0,0 +1,3 @@\n+Add negation-query operators string.isNot and selection.none.\n+New ``plone.app.querystring.operation.string.isNot`` and ``plone.app.querystring.operation.selection.none`` including upgrade steps.\n+[thet]\ndiff --git a/plone/app/querystring/hiddenprofiles.py b/plone/app/querystring/hiddenprofiles.py\nindex 690dc36..7b6c066 100644\n--- a/plone/app/querystring/hiddenprofiles.py\n+++ b/plone/app/querystring/hiddenprofiles.py\n@@ -24,4 +24,5 @@ def getNonInstallableProfiles(self):\n \'plone.app.querystring:upgrade_to_9\',\n \'plone.app.querystring:upgrade_to_10\',\n \'plone.app.querystring:upgrade_to_11\',\n+ \'plone.app.querystring:upgrade_to_14\',\n ]\ndiff --git a/plone/app/querystring/indexmodifiers/query_index_modifiers.py b/plone/app/querystring/indexmodifiers/query_index_modifiers.py\nindex e36de01..050caef 100644\n--- a/plone/app/querystring/indexmodifiers/query_index_modifiers.py\n+++ b/plone/app/querystring/indexmodifiers/query_index_modifiers.py\n@@ -22,9 +22,19 @@ class Subject(object):\n """\n \n def __call__(self, value):\n- query = value[\'query\']\n+ if not six.PY2:\n+ return (\'Subject\', value)\n+\n+ # Get the query operator\n+ op = None\n+ if \'query\' in value:\n+ op = \'query\'\n+ elif \'not\' in value:\n+ op = \'not\'\n+\n+ query = value[op]\n # query can be a unicode string or a list of unicode strings.\n- if six.PY2 and isinstance(query, six.text_type):\n+ if isinstance(query, six.text_type):\n query = query.encode("utf-8")\n elif isinstance(query, list):\n # We do not want to change the collections\' own query string,\n@@ -34,13 +44,13 @@ def __call__(self, value):\n # unicode strings\n i = 0\n for item in copy_of_query:\n- if six.PY2 and isinstance(item, six.text_type):\n+ if isinstance(item, six.text_type):\n copy_of_query[i] = item.encode("utf-8")\n i += 1\n query = copy_of_query\n else:\n pass\n- value[\'query\'] = query\n+ value[op] = query\n return (\'Subject\', value)\n \n \ndiff --git a/plone/app/querystring/profiles.zcml b/plone/app/querystring/profiles.zcml\nindex 558d035..c80de26 100644\n--- a/plone/app/querystring/profiles.zcml\n+++ b/plone/app/querystring/profiles.zcml\n@@ -81,4 +81,12 @@\n provides="Products.GenericSetup.interfaces.EXTENSION"\n />\n \n+ \n+\n \ndiff --git a/plone/app/querystring/profiles/default/metadata.xml b/plone/app/querystring/profiles/default/metadata.xml\nindex f88f99c..610f8d1 100644\n--- a/plone/app/querystring/profiles/default/metadata.xml\n+++ b/plone/app/querystring/profiles/default/metadata.xml\n@@ -1,6 +1,6 @@\n \n \n- 13\n+ 14\n \n profile-plone.app.registry:default\n \ndiff --git a/plone/app/querystring/profiles/default/registry.xml b/plone/app/querystring/profiles/default/registry.xml\nindex bc83015..db23b80 100644\n--- a/plone/app/querystring/profiles/default/registry.xml\n+++ b/plone/app/querystring/profiles/default/registry.xml\n@@ -186,6 +186,14 @@\n StringWidget\n \n \n+ \n+ Is not\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ StringWidget\n+ \n+\n \n Is\n@@ -234,6 +242,14 @@\n MultipleSelectionWidget\n \n \n+ \n+ Matches none of\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ MultipleSelectionWidget\n+ \n+\n \n Matches all of\n@@ -265,6 +281,7 @@\n True\n \n plone.app.querystring.operation.string.is\n+ plone.app.querystring.operation.string.isNot\n \n Metadata\n \n@@ -299,6 +316,7 @@\n \n plone.app.querystring.operation.string.currentUser\n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.Users\n Metadata\n@@ -471,6 +489,7 @@\n False\n \n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.ReallyUserFriendlyTypes\n Metadata\n@@ -497,6 +516,7 @@\n True\n \n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.WorkflowStates\n Metadata\n@@ -523,6 +543,7 @@\n \n plone.app.querystring.operation.string.contains\n plone.app.querystring.operation.string.is\n+ plone.app.querystring.operation.string.isNot\n \n Text\n \n@@ -557,6 +578,7 @@\n \n plone.app.querystring.operation.selection.any\n plone.app.querystring.operation.selection.all\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.Keywords\n Text\ndiff --git a/plone/app/querystring/profiles/upgrades/to_14/registry.xml b/plone/app/querystring/profiles/upgrades/to_14/registry.xml\nnew file mode 100644\nindex 0000000..1c4d6c7\n--- /dev/null\n+++ b/plone/app/querystring/profiles/upgrades/to_14/registry.xml\n@@ -0,0 +1,72 @@\n+\n+\n+ \n+ Is not\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ StringWidget\n+ \n+\n+ \n+ Matches none of\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ MultipleSelectionWidget\n+ \n+\n+ \n+ \n+ plone.app.querystring.operation.string.isNot\n+ \n+ \n+\n+ \n+ \n+ plone.app.querystring.operation.string.isNot\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\ndiff --git a/plone/app/querystring/queryparser.py b/plone/app/querystring/queryparser.py\nindex 55124d7..97bd757 100644\n--- a/plone/app/querystring/queryparser.py\n+++ b/plone/app/querystring/queryparser.py\n@@ -75,6 +75,10 @@ def _contains(context, row):\n return _equal(context, row)\n \n \n+def _excludes(context, row):\n+ return {row.index: {\'not\': row.values}}\n+\n+\n def _equal(context, row):\n return {row.index: {\'query\': row.values, }}\n \ndiff --git a/plone/app/querystring/tests/testIndexmodifiers.py b/plone/app/querystring/tests/testIndexmodifiers.py\nindex 27841b1..6a93f90 100644\n--- a/plone/app/querystring/tests/testIndexmodifiers.py\n+++ b/plone/app/querystring/tests/testIndexmodifiers.py\n@@ -1,6 +1,6 @@\n # -*- coding: utf-8 -*-\n-from DateTime import DateTime\n from datetime import datetime\n+from DateTime import DateTime\n from plone.app.querystring.indexmodifiers import query_index_modifiers\n \n import unittest\n@@ -13,6 +13,16 @@ def test_subject_encoded(self):\n query_index_modifiers.Subject()({\'query\': u\'foobar\'}),\n (\'Subject\', {\'query\': u\'foobar\'}))\n \n+ def test_subject_encoded__list(self):\n+ self.assertEqual(\n+ query_index_modifiers.Subject()({\'query\': [u\'foobar\']}),\n+ (\'Subject\', {\'query\': [u\'foobar\']}))\n+\n+ def test_subject_encoded__list_not(self):\n+ self.assertEqual(\n+ query_index_modifiers.Subject()({\'not\': [u\'foobar\']}),\n+ (\'Subject\', {\'not\': [\'foobar\']}))\n+\n def test_date_modifier(self):\n modifier = query_index_modifiers.start()\n self.assertTrue(\n@@ -32,6 +42,7 @@ def test_date_modifier_list(self):\n def test_date_modifier_list_DateTime(self):\n """Test a case with largerThanRelativeDate operatiors, where\n plone.app.querystring.querybuilder parses a querystring like this one:\n+\n >>> query\n [{\n u\'i\': u\'end\',\ndiff --git a/plone/app/querystring/tests/testQueryBuilder.py b/plone/app/querystring/tests/testQueryBuilder.py\nindex 3565ecc..a112304 100644\n--- a/plone/app/querystring/tests/testQueryBuilder.py\n+++ b/plone/app/querystring/tests/testQueryBuilder.py\n@@ -69,6 +69,35 @@ def testMakeQuery(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testQueryStringIs(self):\n+ query = [{\n+ \'i\': \'sortable_title\',\n+ \'o\': \'plone.app.querystring.operation.string.is\',\n+ \'v\': \'collectionstestpage\',\n+ }]\n+\n+ # Test normal, without custom_query.\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage\')\n+\n+ def testQueryStringIsNot(self):\n+ query = [{\n+ \'i\': \'portal_type\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': \'Plone Site\',\n+ }, {\n+ \'i\': \'sortable_title\',\n+ \'o\': \'plone.app.querystring.operation.string.isNot\',\n+ \'v\': \'collectionstestpage\',\n+ }]\n+\n+ # Test normal, without custom_query.\n+ results = self.querybuilder._makequery(query=query)\n+ print([it.Title() for it in results])\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Test Folder\')\n+\n def testMakeQueryWithSubject(self):\n self.testpage.setSubject([\'Lorem\'])\n self.testpage.reindexObject()\n@@ -83,6 +112,22 @@ def testMakeQueryWithSubject(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testMakeQueryWithSubjectNot(self):\n+ self.folder.setSubject([\'Ipsum\'])\n+ self.folder.reindexObject()\n+ self.testpage.setSubject([\'Lorem\'])\n+ self.testpage.reindexObject()\n+ query = [{\n+ \'i\': \'Subject\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': \'Lorem\',\n+ }]\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(\n+ results[0].getURL(),\n+ \'http://nohost/plone/testfolder\')\n+\n def testMakeQueryWithMultipleSubject(self):\n self.testpage.setSubject([\'Lorem\'])\n self.testpage.reindexObject()\n@@ -97,6 +142,22 @@ def testMakeQueryWithMultipleSubject(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testMakeQueryWithMultipleSubjectNot(self):\n+ self.folder.setSubject([\'Ipsum\'])\n+ self.folder.reindexObject()\n+ self.testpage.setSubject([\'Lorem\'])\n+ self.testpage.reindexObject()\n+ query = [{\n+ \'i\': \'Subject\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': [\'Lorem\', \'Dolor\'],\n+ }]\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(\n+ results[0].getURL(),\n+ \'http://nohost/plone/testfolder\')\n+\n def testMakeQueryWithSubjectWithSpecialCharacters(self):\n self.testpage.setSubject([\'\xc3\x84\xc3\xbc\xc3\xb6\'])\n self.testpage.reindexObject()\ndiff --git a/plone/app/querystring/tests/testRegistryIntegration.py b/plone/app/querystring/tests/testRegistryIntegration.py\nindex 05e5cfb..1fda4c5 100644\n--- a/plone/app/querystring/tests/testRegistryIntegration.py\n+++ b/plone/app/querystring/tests/testRegistryIntegration.py\n@@ -52,11 +52,14 @@ def test_getId(self):\n self.assertEqual(registry[prefix + ".title"], "Short name (id)")\n \n operations = registry[prefix + ".operations"]\n- self.assertEqual(len(operations), 1)\n+ self.assertEqual(len(operations), 2)\n \n equal = \'plone.app.querystring.operation.string.is\'\n self.assertTrue(equal in operations)\n \n+ exclude = \'plone.app.querystring.operation.string.isNot\'\n+ self.assertTrue(exclude in operations)\n+\n self.assertEqual(registry[prefix + ".description"],\n "The short name of an item (used in the url)")\n self.assertEqual(registry[prefix + ".enabled"], True)\ndiff --git a/plone/app/querystring/upgrades.zcml b/plone/app/querystring/upgrades.zcml\nindex 7506ded..dfd40f4 100644\n--- a/plone/app/querystring/upgrades.zcml\n+++ b/plone/app/querystring/upgrades.zcml\n@@ -137,4 +137,14 @@\n />\n \n \n+ \n+ \n+ \n+\n \n' + +Repository: plone.app.querystring + + +Branch: refs/heads/master +Date: 2022-05-24T16:51:20+02:00 +Author: Jens W. Klein (jensens) +Commit: https://github.com/plone/plone.app.querystring/commit/a1e7091736b28f3cbe60288d4e3e875c3e5752df + +Merge pull request #110 from plone/thet-excludes + +Add negative query support: string.isNot and selection.none Files changed: -A news/103.bugfix -M plone/app/querystring/querybuilder.py +A news/110.feature +A plone/app/querystring/profiles/upgrades/to_14/registry.xml +M plone/app/querystring/hiddenprofiles.py +M plone/app/querystring/indexmodifiers/query_index_modifiers.py +M plone/app/querystring/profiles.zcml +M plone/app/querystring/profiles/default/metadata.xml +M plone/app/querystring/profiles/default/registry.xml +M plone/app/querystring/queryparser.py +M plone/app/querystring/tests/testIndexmodifiers.py M plone/app/querystring/tests/testQueryBuilder.py +M plone/app/querystring/tests/testRegistryIntegration.py +M plone/app/querystring/upgrades.zcml -b'diff --git a/news/103.bugfix b/news/103.bugfix\nnew file mode 100644\nindex 0000000..f7791b9\n--- /dev/null\n+++ b/news/103.bugfix\n@@ -0,0 +1 @@\n+Fix how to merge custom_query with parsedquery without overriding values. [cekk]\ndiff --git a/plone/app/querystring/querybuilder.py b/plone/app/querystring/querybuilder.py\nindex 31689be..8cd3c39 100644\n--- a/plone/app/querystring/querybuilder.py\n+++ b/plone/app/querystring/querybuilder.py\n@@ -160,11 +160,18 @@ def _makequery(self, query=None, batch=False, b_start=0, b_size=30,\n # Update the parsed query with an extra query dictionary. This may\n # override the parsed query. The custom_query is a dictonary of\n # index names and their associated query values.\n- parsedquery.update(custom_query)\n+ for key in custom_query:\n+ if (\n+ isinstance(parsedquery.get(key), dict)\n+ and isinstance(custom_query.get(key), dict)\n+ ):\n+ parsedquery[key].update(custom_query[key])\n+ continue\n+ parsedquery[key] = custom_query[key]\n empty_query = False\n \n # filter bad term and operator in query\n- parsedquery = self.filter_query(parsedquery)\n+ parsedquery = self.filter_query(parsedquery)\n results = []\n if not empty_query:\n results = catalog(**parsedquery)\ndiff --git a/plone/app/querystring/tests/testQueryBuilder.py b/plone/app/querystring/tests/testQueryBuilder.py\nindex 419a531..3565ecc 100644\n--- a/plone/app/querystring/tests/testQueryBuilder.py\n+++ b/plone/app/querystring/tests/testQueryBuilder.py\n@@ -188,6 +188,45 @@ def testQueryBuilderCustomQuery(self):\n self.assertEqual(len(results), 1)\n self.assertEqual(results[0].Title(), \'Test Folder\')\n \n+ def testQueryBuilderCustomQueryDoNotOverrideValues(self):\n+ """Test if custom queries do not override values if they are dicts\n+ """\n+ self.portal.invokeFactory("Document",\n+ "collectionstestpage-2",\n+ title="Collectionstestpage 2")\n+ testpage2 = self.portal[\'collectionstestpage-2\']\n+ query = [{\n+ \'i\': \'UID\',\n+ \'o\': \'plone.app.querystring.operation.string.is\',\n+ \'v\': [self.testpage.UID(), testpage2.UID()],\n+ }]\n+\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 2)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage\')\n+ self.assertEqual(results[1].Title(), \'Collectionstestpage 2\')\n+\n+ # if we add new values to the query, they should not be overwritten\n+ results = self.querybuilder._makequery(\n+ query=query,\n+ custom_query={\'UID\': {\'not\': testpage2.UID()}})\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage\')\n+\n+ # if we add the same values to the query, they should be overwritten\n+ results = self.querybuilder._makequery(\n+ query=query,\n+ custom_query={\'UID\': {\'query\': testpage2.UID()}})\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage 2\')\n+\n+ # add simple custom query\n+ results = self.querybuilder._makequery(\n+ query=query,\n+ custom_query={\'UID\': testpage2.UID()})\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage 2\')\n+\n \n class TestQuerybuilderResultTypes(unittest.TestCase):\n \n' +b'diff --git a/news/110.feature b/news/110.feature\nnew file mode 100644\nindex 0000000..d9709b7\n--- /dev/null\n+++ b/news/110.feature\n@@ -0,0 +1,3 @@\n+Add negation-query operators string.isNot and selection.none.\n+New ``plone.app.querystring.operation.string.isNot`` and ``plone.app.querystring.operation.selection.none`` including upgrade steps.\n+[thet]\ndiff --git a/plone/app/querystring/hiddenprofiles.py b/plone/app/querystring/hiddenprofiles.py\nindex 690dc36..7b6c066 100644\n--- a/plone/app/querystring/hiddenprofiles.py\n+++ b/plone/app/querystring/hiddenprofiles.py\n@@ -24,4 +24,5 @@ def getNonInstallableProfiles(self):\n \'plone.app.querystring:upgrade_to_9\',\n \'plone.app.querystring:upgrade_to_10\',\n \'plone.app.querystring:upgrade_to_11\',\n+ \'plone.app.querystring:upgrade_to_14\',\n ]\ndiff --git a/plone/app/querystring/indexmodifiers/query_index_modifiers.py b/plone/app/querystring/indexmodifiers/query_index_modifiers.py\nindex e36de01..050caef 100644\n--- a/plone/app/querystring/indexmodifiers/query_index_modifiers.py\n+++ b/plone/app/querystring/indexmodifiers/query_index_modifiers.py\n@@ -22,9 +22,19 @@ class Subject(object):\n """\n \n def __call__(self, value):\n- query = value[\'query\']\n+ if not six.PY2:\n+ return (\'Subject\', value)\n+\n+ # Get the query operator\n+ op = None\n+ if \'query\' in value:\n+ op = \'query\'\n+ elif \'not\' in value:\n+ op = \'not\'\n+\n+ query = value[op]\n # query can be a unicode string or a list of unicode strings.\n- if six.PY2 and isinstance(query, six.text_type):\n+ if isinstance(query, six.text_type):\n query = query.encode("utf-8")\n elif isinstance(query, list):\n # We do not want to change the collections\' own query string,\n@@ -34,13 +44,13 @@ def __call__(self, value):\n # unicode strings\n i = 0\n for item in copy_of_query:\n- if six.PY2 and isinstance(item, six.text_type):\n+ if isinstance(item, six.text_type):\n copy_of_query[i] = item.encode("utf-8")\n i += 1\n query = copy_of_query\n else:\n pass\n- value[\'query\'] = query\n+ value[op] = query\n return (\'Subject\', value)\n \n \ndiff --git a/plone/app/querystring/profiles.zcml b/plone/app/querystring/profiles.zcml\nindex 558d035..c80de26 100644\n--- a/plone/app/querystring/profiles.zcml\n+++ b/plone/app/querystring/profiles.zcml\n@@ -81,4 +81,12 @@\n provides="Products.GenericSetup.interfaces.EXTENSION"\n />\n \n+ \n+\n \ndiff --git a/plone/app/querystring/profiles/default/metadata.xml b/plone/app/querystring/profiles/default/metadata.xml\nindex f88f99c..610f8d1 100644\n--- a/plone/app/querystring/profiles/default/metadata.xml\n+++ b/plone/app/querystring/profiles/default/metadata.xml\n@@ -1,6 +1,6 @@\n \n \n- 13\n+ 14\n \n profile-plone.app.registry:default\n \ndiff --git a/plone/app/querystring/profiles/default/registry.xml b/plone/app/querystring/profiles/default/registry.xml\nindex bc83015..db23b80 100644\n--- a/plone/app/querystring/profiles/default/registry.xml\n+++ b/plone/app/querystring/profiles/default/registry.xml\n@@ -186,6 +186,14 @@\n StringWidget\n \n \n+ \n+ Is not\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ StringWidget\n+ \n+\n \n Is\n@@ -234,6 +242,14 @@\n MultipleSelectionWidget\n \n \n+ \n+ Matches none of\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ MultipleSelectionWidget\n+ \n+\n \n Matches all of\n@@ -265,6 +281,7 @@\n True\n \n plone.app.querystring.operation.string.is\n+ plone.app.querystring.operation.string.isNot\n \n Metadata\n \n@@ -299,6 +316,7 @@\n \n plone.app.querystring.operation.string.currentUser\n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.Users\n Metadata\n@@ -471,6 +489,7 @@\n False\n \n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.ReallyUserFriendlyTypes\n Metadata\n@@ -497,6 +516,7 @@\n True\n \n plone.app.querystring.operation.selection.any\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.WorkflowStates\n Metadata\n@@ -523,6 +543,7 @@\n \n plone.app.querystring.operation.string.contains\n plone.app.querystring.operation.string.is\n+ plone.app.querystring.operation.string.isNot\n \n Text\n \n@@ -557,6 +578,7 @@\n \n plone.app.querystring.operation.selection.any\n plone.app.querystring.operation.selection.all\n+ plone.app.querystring.operation.selection.none\n \n plone.app.vocabularies.Keywords\n Text\ndiff --git a/plone/app/querystring/profiles/upgrades/to_14/registry.xml b/plone/app/querystring/profiles/upgrades/to_14/registry.xml\nnew file mode 100644\nindex 0000000..1c4d6c7\n--- /dev/null\n+++ b/plone/app/querystring/profiles/upgrades/to_14/registry.xml\n@@ -0,0 +1,72 @@\n+\n+\n+ \n+ Is not\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ StringWidget\n+ \n+\n+ \n+ Matches none of\n+ Tip: you can use * to autocomplete.\n+ plone.app.querystring.queryparser._excludes\n+ MultipleSelectionWidget\n+ \n+\n+ \n+ \n+ plone.app.querystring.operation.string.isNot\n+ \n+ \n+\n+ \n+ \n+ plone.app.querystring.operation.string.isNot\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\n+ \n+ \n+ plone.app.querystring.operation.selection.none\n+ \n+ \n+\n+\ndiff --git a/plone/app/querystring/queryparser.py b/plone/app/querystring/queryparser.py\nindex 55124d7..97bd757 100644\n--- a/plone/app/querystring/queryparser.py\n+++ b/plone/app/querystring/queryparser.py\n@@ -75,6 +75,10 @@ def _contains(context, row):\n return _equal(context, row)\n \n \n+def _excludes(context, row):\n+ return {row.index: {\'not\': row.values}}\n+\n+\n def _equal(context, row):\n return {row.index: {\'query\': row.values, }}\n \ndiff --git a/plone/app/querystring/tests/testIndexmodifiers.py b/plone/app/querystring/tests/testIndexmodifiers.py\nindex 27841b1..6a93f90 100644\n--- a/plone/app/querystring/tests/testIndexmodifiers.py\n+++ b/plone/app/querystring/tests/testIndexmodifiers.py\n@@ -1,6 +1,6 @@\n # -*- coding: utf-8 -*-\n-from DateTime import DateTime\n from datetime import datetime\n+from DateTime import DateTime\n from plone.app.querystring.indexmodifiers import query_index_modifiers\n \n import unittest\n@@ -13,6 +13,16 @@ def test_subject_encoded(self):\n query_index_modifiers.Subject()({\'query\': u\'foobar\'}),\n (\'Subject\', {\'query\': u\'foobar\'}))\n \n+ def test_subject_encoded__list(self):\n+ self.assertEqual(\n+ query_index_modifiers.Subject()({\'query\': [u\'foobar\']}),\n+ (\'Subject\', {\'query\': [u\'foobar\']}))\n+\n+ def test_subject_encoded__list_not(self):\n+ self.assertEqual(\n+ query_index_modifiers.Subject()({\'not\': [u\'foobar\']}),\n+ (\'Subject\', {\'not\': [\'foobar\']}))\n+\n def test_date_modifier(self):\n modifier = query_index_modifiers.start()\n self.assertTrue(\n@@ -32,6 +42,7 @@ def test_date_modifier_list(self):\n def test_date_modifier_list_DateTime(self):\n """Test a case with largerThanRelativeDate operatiors, where\n plone.app.querystring.querybuilder parses a querystring like this one:\n+\n >>> query\n [{\n u\'i\': u\'end\',\ndiff --git a/plone/app/querystring/tests/testQueryBuilder.py b/plone/app/querystring/tests/testQueryBuilder.py\nindex 3565ecc..a112304 100644\n--- a/plone/app/querystring/tests/testQueryBuilder.py\n+++ b/plone/app/querystring/tests/testQueryBuilder.py\n@@ -69,6 +69,35 @@ def testMakeQuery(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testQueryStringIs(self):\n+ query = [{\n+ \'i\': \'sortable_title\',\n+ \'o\': \'plone.app.querystring.operation.string.is\',\n+ \'v\': \'collectionstestpage\',\n+ }]\n+\n+ # Test normal, without custom_query.\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Collectionstestpage\')\n+\n+ def testQueryStringIsNot(self):\n+ query = [{\n+ \'i\': \'portal_type\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': \'Plone Site\',\n+ }, {\n+ \'i\': \'sortable_title\',\n+ \'o\': \'plone.app.querystring.operation.string.isNot\',\n+ \'v\': \'collectionstestpage\',\n+ }]\n+\n+ # Test normal, without custom_query.\n+ results = self.querybuilder._makequery(query=query)\n+ print([it.Title() for it in results])\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(results[0].Title(), \'Test Folder\')\n+\n def testMakeQueryWithSubject(self):\n self.testpage.setSubject([\'Lorem\'])\n self.testpage.reindexObject()\n@@ -83,6 +112,22 @@ def testMakeQueryWithSubject(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testMakeQueryWithSubjectNot(self):\n+ self.folder.setSubject([\'Ipsum\'])\n+ self.folder.reindexObject()\n+ self.testpage.setSubject([\'Lorem\'])\n+ self.testpage.reindexObject()\n+ query = [{\n+ \'i\': \'Subject\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': \'Lorem\',\n+ }]\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(\n+ results[0].getURL(),\n+ \'http://nohost/plone/testfolder\')\n+\n def testMakeQueryWithMultipleSubject(self):\n self.testpage.setSubject([\'Lorem\'])\n self.testpage.reindexObject()\n@@ -97,6 +142,22 @@ def testMakeQueryWithMultipleSubject(self):\n results[0].getURL(),\n \'http://nohost/plone/collectionstestpage\')\n \n+ def testMakeQueryWithMultipleSubjectNot(self):\n+ self.folder.setSubject([\'Ipsum\'])\n+ self.folder.reindexObject()\n+ self.testpage.setSubject([\'Lorem\'])\n+ self.testpage.reindexObject()\n+ query = [{\n+ \'i\': \'Subject\',\n+ \'o\': \'plone.app.querystring.operation.selection.none\',\n+ \'v\': [\'Lorem\', \'Dolor\'],\n+ }]\n+ results = self.querybuilder._makequery(query=query)\n+ self.assertEqual(len(results), 1)\n+ self.assertEqual(\n+ results[0].getURL(),\n+ \'http://nohost/plone/testfolder\')\n+\n def testMakeQueryWithSubjectWithSpecialCharacters(self):\n self.testpage.setSubject([\'\xc3\x84\xc3\xbc\xc3\xb6\'])\n self.testpage.reindexObject()\ndiff --git a/plone/app/querystring/tests/testRegistryIntegration.py b/plone/app/querystring/tests/testRegistryIntegration.py\nindex 05e5cfb..1fda4c5 100644\n--- a/plone/app/querystring/tests/testRegistryIntegration.py\n+++ b/plone/app/querystring/tests/testRegistryIntegration.py\n@@ -52,11 +52,14 @@ def test_getId(self):\n self.assertEqual(registry[prefix + ".title"], "Short name (id)")\n \n operations = registry[prefix + ".operations"]\n- self.assertEqual(len(operations), 1)\n+ self.assertEqual(len(operations), 2)\n \n equal = \'plone.app.querystring.operation.string.is\'\n self.assertTrue(equal in operations)\n \n+ exclude = \'plone.app.querystring.operation.string.isNot\'\n+ self.assertTrue(exclude in operations)\n+\n self.assertEqual(registry[prefix + ".description"],\n "The short name of an item (used in the url)")\n self.assertEqual(registry[prefix + ".enabled"], True)\ndiff --git a/plone/app/querystring/upgrades.zcml b/plone/app/querystring/upgrades.zcml\nindex 7506ded..dfd40f4 100644\n--- a/plone/app/querystring/upgrades.zcml\n+++ b/plone/app/querystring/upgrades.zcml\n@@ -137,4 +137,14 @@\n />\n \n \n+ \n+ \n+ \n+\n \n'