Browse Source

Add support for double quotes query string searches

- Fixes #9951
4the4ryushin 2 years ago
parent
commit
66a086da29

+ 1 - 0
CHANGELOG.txt

@@ -19,6 +19,7 @@ Changelog
  * Switch lock/unlock side panel toggle to a switch, with more appropriate confirmation message status (Sage Abdullah)
  * Ensure that changed or cleared selection from choosers will dispatch a DOM `change` event (George Sakkis)
  * Add the ability to disable model indexing by setting `search_fields = []` (Daniel Kirkham)
+ * Enhance `wagtail.search.utils.parse_query_string` to allow inner single quotes for key/value parsing (Aman Pandey)
  * Fix: Ensure `label_format` on StructBlock gracefully handles missing variables (Aadi jindal)
  * Fix: Adopt a no-JavaScript and more accessible solution for the 'Reset to default' switch to Gravatar when editing user profile (Loveth Omokaro)
  * Fix: Ensure `Site.get_site_root_paths` works on cache backends that do not preserve Python objects (Jaap Roes)

+ 1 - 0
docs/releases/5.0.md

@@ -31,6 +31,7 @@ Support for adding custom validation logic to StreamField blocks has been formal
  * Switch lock/unlock side panel toggle to a switch, with more appropriate confirmation message status (Sage Abdullah)
  * Ensure that changed or cleared selection from choosers will dispatch a DOM `change` event (George Sakkis)
  * Add the ability to [disable model indexing](wagtailsearch_disable_indexing) by setting `search_fields = []` (Daniel Kirkham)
+ * Enhance `wagtail.search.utils.parse_query_string` to allow inner single quotes for key/value parsing (Aman Pandey)
 
 ### Bug fixes
 

+ 5 - 2
docs/topics/search/searching.md

@@ -272,11 +272,14 @@ For example:
 
 ```python
 >>> from wagtail.search.utils import parse_query_string
->>> filters, query = parse_query_string('my query string "this is a phrase" this-is-a:filter', operator='and')
+>>> filters, query = parse_query_string('my query string "this is a phrase" this_is_a:filter', operator='and')
+
+# Alternatively..
+# filters, query = parse_query_string("my query string 'this is a phrase' this_is_a:filter", operator='and')
 
 >>> filters
 {
-    'this-is-a': 'filter',
+    'this_is_a': 'filter',
 }
 
 >>> query

+ 58 - 0
wagtail/search/tests/test_queries.py

@@ -235,6 +235,13 @@ class TestSeparateFiltersFromQuery(SimpleTestCase):
         self.assertDictEqual(filters, {"author": "foo bar", "bar": "two beers"})
         self.assertEqual(query, "hello world")
 
+        filters, query = separate_filters_from_query(
+            "author:'foo bar' hello world bar:'two beers'"
+        )
+
+        self.assertDictEqual(filters, {"author": "foo bar", "bar": "two beers"})
+        self.assertEqual(query, "hello world")
+
 
 class TestParseQueryString(SimpleTestCase):
     def test_simple_query(self):
@@ -249,6 +256,11 @@ class TestParseQueryString(SimpleTestCase):
         self.assertDictEqual(filters, {})
         self.assertEqual(repr(query), repr(Phrase("hello world")))
 
+        filters, query = parse_query_string("'hello world'")
+
+        self.assertDictEqual(filters, {})
+        self.assertEqual(repr(query), repr(Phrase("hello world")))
+
     def test_with_simple_and_phrase(self):
         filters, query = parse_query_string('this is simple "hello world"')
 
@@ -257,6 +269,13 @@ class TestParseQueryString(SimpleTestCase):
             repr(query), repr(And([PlainText("this is simple"), Phrase("hello world")]))
         )
 
+        filters, query = parse_query_string("this is simple 'hello world'")
+
+        self.assertDictEqual(filters, {})
+        self.assertEqual(
+            repr(query), repr(And([PlainText("this is simple"), Phrase("hello world")]))
+        )
+
     def test_operator(self):
         filters, query = parse_query_string(
             'this is simple "hello world"', operator="or"
@@ -270,18 +289,40 @@ class TestParseQueryString(SimpleTestCase):
             ),
         )
 
+        filters, query = parse_query_string(
+            "this is simple 'hello world'", operator="or"
+        )
+
+        self.assertDictEqual(filters, {})
+        self.assertEqual(
+            repr(query),
+            repr(
+                Or([PlainText("this is simple", operator="or"), Phrase("hello world")])
+            ),
+        )
+
     def test_with_phrase_unclosed(self):
         filters, query = parse_query_string('"hello world')
 
         self.assertDictEqual(filters, {})
         self.assertEqual(repr(query), repr(Phrase("hello world")))
 
+        filters, query = parse_query_string("'hello world")
+
+        self.assertDictEqual(filters, {})
+        self.assertEqual(repr(query), repr(Phrase("hello world")))
+
     def test_phrase_with_filter(self):
         filters, query = parse_query_string('"hello world" author:"foo bar" bar:beer')
 
         self.assertDictEqual(filters, {"author": "foo bar", "bar": "beer"})
         self.assertEqual(repr(query), repr(Phrase("hello world")))
 
+        filters, query = parse_query_string("'hello world' author:'foo bar' bar:beer")
+
+        self.assertDictEqual(filters, {"author": "foo bar", "bar": "beer"})
+        self.assertEqual(repr(query), repr(Phrase("hello world")))
+
     def test_multiple_phrases(self):
         filters, query = parse_query_string('"hello world" "hi earth"')
 
@@ -289,6 +330,23 @@ class TestParseQueryString(SimpleTestCase):
             repr(query), repr(And([Phrase("hello world"), Phrase("hi earth")]))
         )
 
+        filters, query = parse_query_string("'hello world' 'hi earth'")
+
+        self.assertEqual(
+            repr(query), repr(And([Phrase("hello world"), Phrase("hi earth")]))
+        )
+
+    def test_mixed_phrases_with_filters(self):
+        filters, query = parse_query_string(
+            """"lord of the rings" army_1:"elves" army_2:'humans'"""
+        )
+
+        self.assertDictEqual(filters, {"army_1": "elves", "army_2": "humans"})
+        self.assertEqual(
+            repr(query),
+            repr(Phrase("lord of the rings")),
+        )
+
 
 class TestBalancedReduce(SimpleTestCase):
     # For simple values, this should behave exactly the same as Pythons reduce()

+ 10 - 3
wagtail/search/utils.py

@@ -82,12 +82,14 @@ def normalise_query_string(query_string):
 
 
 def separate_filters_from_query(query_string):
-    filters_regexp = r'(\w+):(\w+|"[^"]+")'
+    filters_regexp = r'(\w+):(\w+|"[^"]+"|\'[^\']+\')'
 
     filters = {}
     for match_object in re.finditer(filters_regexp, query_string):
         key, value = match_object.groups()
-        filters[key] = value.strip('"')
+        filters[key] = (
+            value.strip('"') if value.strip('"') is not value else value.strip("'")
+        )
 
     query_string = re.sub(filters_regexp, "", query_string).strip()
 
@@ -112,7 +114,12 @@ def parse_query_string(query_string, operator=None, zero_terms=MATCH_NONE):
 
     is_phrase = False
     tokens = []
-    for part in query_string.split('"'):
+    if '"' in query_string:
+        parts = query_string.split('"')
+    else:
+        parts = query_string.split("'")
+
+    for part in parts:
         part = part.strip()
 
         if part: