We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Search / Criteria"Exact" overwrites 'Add" button
The search is a great feature (quick to fill, powerfull  - with advanced criterias - and results are fast and reliable) but I have noticed an issue in the layout : "Exact" (which seems to be an untranslatable string yet) overwrite the "Add" button (at least in french, the full french label is "Ajouter"); see:  ![Search](https://user-images.githubusercontent.com/1452146/78504660-e0368a00-776e-11ea-9ede-ce77263e4107.jpg)

page https://hosted.weblate.org/projects/weblate/master/#search 


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `weblate/utils/search.py`
Content:
```
1 #
2 # Copyright © 2012 - 2020 Michal Čihař <michal@cihar.com>
3 #
4 # This file is part of Weblate <https://weblate.org/>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
18 #
19 
20 
21 import re
22 from functools import lru_cache, reduce
23 
24 import whoosh.qparser
25 import whoosh.qparser.dateparse
26 import whoosh.query
27 from django.db.models import Q
28 from django.utils import timezone
29 from django.utils.translation import gettext as _
30 from jellyfish import damerau_levenshtein_distance
31 from whoosh.fields import BOOLEAN, DATETIME, NUMERIC, TEXT, Schema
32 
33 from weblate.trans.util import PLURAL_SEPARATOR
34 from weblate.utils.state import (
35     STATE_APPROVED,
36     STATE_NAMES,
37     STATE_READONLY,
38     STATE_TRANSLATED,
39 )
40 
41 
42 class Comparer:
43     """String comparer abstraction.
44 
45     The reason is to be able to change implementation.
46     """
47 
48     def similarity(self, first, second):
49         """Returns string similarity in range 0 - 100%."""
50         try:
51             distance = damerau_levenshtein_distance(first, second)
52             return int(
53                 100 * (1.0 - (float(distance) / max(len(first), len(second), 1)))
54             )
55         except MemoryError:
56             # Too long string, mark them as not much similar
57             return 50
58 
59 
60 class QuotePlugin(whoosh.qparser.SingleQuotePlugin):
61     """Single and double quotes to specify a term."""
62 
63     expr = r"(^|(?<=\W))['\"](?P<text>.*?)['\"](?=\s|\]|[)}]|$)"
64 
65 
66 class Exact(whoosh.query.Term):
67     """Class for queries with exact operator."""
68 
69     pass
70 
71 
72 class ExactPlugin(QuotePlugin):
73     """Exact match plugin with quotes to specify an exact term."""
74 
75     class ExactNode(whoosh.qparser.syntax.TextNode):
76         qclass = Exact
77 
78         def r(self):
79             return "Exact %r" % self.text
80 
81     expr = r"\=(^|(?<=\W))['\"](?P<text>.*?)['\"](?=\s|\]|[)}]|$)"
82     nodetype = ExactNode
83 
84 
85 class GtLtPlugin(whoosh.qparser.GtLtPlugin):
86     """GtLt plugin taggin only after ":"."""
87 
88     def match(self, parser, text, pos):
89         if pos == 0 or text[pos - 1] != ":":
90             return None
91         return super().match(parser, text, pos)
92 
93 
94 class DateParser(whoosh.qparser.dateparse.English):
95     def setup(self):
96         super().setup()
97         # We prefer simple parser prior to datetime
98         # This might not be necessary after following issue is fixed:
99         # https://github.com/whoosh-community/whoosh/issues/552
100         self.bundle.elements = (self.plusdate, self.simple, self.datetime)
101 
102 
103 class NumberField(NUMERIC):
104     def to_bytes(self, x, shift=0):
105         return int(x)
106 
107 
108 class StateField(NumberField):
109     def parse_query(self, fieldname, qstring, boost=1.0):
110         return super().parse_query(fieldname, state_to_int(qstring), boost)
111 
112     def parse_range(self, fieldname, start, end, startexcl, endexcl, boost=1.0):
113         return super().parse_range(
114             fieldname, state_to_int(start), state_to_int(end), startexcl, endexcl, boost
115         )
116 
117 
118 def state_to_int(text):
119     if text is None:
120         return None
121     try:
122         return STATE_NAMES[text]
123     except KeyError:
124         raise ValueError(_("Unsupported state: {}").format(text))
125 
126 
127 class QueryParser(whoosh.qparser.QueryParser):
128     """Weblate query parser, differences to Whoosh default.
129 
130     - no phrase plugin
131     - <> operators support
132     - double and single quotes behave identical
133     - multifield lookup for unspecified terms
134     """
135 
136     def __init__(self):
137         # Define fields for parsing
138         fields = {
139             # Unit fields
140             "source": TEXT,
141             "target": TEXT,
142             "context": TEXT,
143             "note": TEXT,
144             "location": TEXT,
145             "priority": NumberField,
146             "added": DATETIME,
147             "state": StateField,
148             "pending": BOOLEAN,
149             "has": TEXT,
150             "is": TEXT,
151             # Language
152             "language": TEXT,
153             # Change fields
154             "changed": DATETIME,
155             "changed_by": TEXT,
156             # Unit data
157             "check": TEXT,
158             "ignored_check": TEXT,
159             "suggestion": TEXT,
160             "suggestion_author": TEXT,
161             "comment": TEXT,
162             "comment_author": TEXT,
163             "label": TEXT,
164         }
165         schema = Schema(**fields)
166 
167         # List of plugins
168         plugins = [
169             whoosh.qparser.WhitespacePlugin(),
170             QuotePlugin(),
171             whoosh.qparser.FieldsPlugin(),
172             whoosh.qparser.RangePlugin(),
173             GtLtPlugin(),
174             ExactPlugin(),
175             whoosh.qparser.RegexPlugin(),
176             whoosh.qparser.GroupPlugin(),
177             whoosh.qparser.OperatorsPlugin(),
178             whoosh.qparser.dateparse.DateParserPlugin(dateparser=DateParser()),
179             whoosh.qparser.MultifieldPlugin(["source", "target", "context"]),
180         ]
181         super().__init__(None, schema, plugins=plugins)
182 
183     def term_query(
184         self, fieldname, text, termclass, boost=1.0, tokenize=True, removestops=True
185     ):
186         if self.schema and fieldname in self.schema:
187             if isinstance(self.schema[fieldname], TEXT):
188                 return termclass(fieldname, text, boost=boost)
189         return super().term_query(
190             fieldname, text, termclass, boost, tokenize, removestops
191         )
192 
193 
194 PARSER = QueryParser()
195 
196 PLAIN_FIELDS = ("source", "target", "context", "note", "location")
197 FIELD_MAP = {"changed": "change__timestamp", "added": "timestamp"}
198 STRING_FIELD_MAP = {"suggestion": "suggestion__target", "comment": "comment__comment"}
199 EXACT_FIELD_MAP = {
200     "check": "check__check",
201     "ignored_check": "check__check",
202     "language": "translation__language__code",
203     "changed_by": "change__author__username",
204     "suggestion_author": "suggestion__user__username",
205     "comment_author": "comment__user__username",
206     "label": "labels__name",
207 }
208 
209 
210 def field_name(field, suffix="substring"):
211     if field in FIELD_MAP:
212         return FIELD_MAP[field]
213     if field in PLAIN_FIELDS:
214         return "{}__{}".format(field, suffix)
215     if field in STRING_FIELD_MAP:
216         return "{}__{}".format(STRING_FIELD_MAP[field], suffix)
217     if field in EXACT_FIELD_MAP:
218         # Change contains to exact, do not change other (for example regex)
219         if suffix == "substring":
220             suffix = "iexact"
221         return "{}__{}".format(EXACT_FIELD_MAP[field], suffix)
222     return field
223 
224 
225 def field_extra(field, query):
226     from weblate.trans.models import Change
227 
228     if field in {"changed", "changed_by"}:
229         return query & Q(change__action__in=Change.ACTIONS_CONTENT)
230     if field == "check":
231         return query & Q(check__ignore=False)
232     if field == "ignored_check":
233         return query & Q(check__ignore=True)
234     return query
235 
236 
237 def range_sql(field, start, end, startexcl, endexcl, conv=int):
238     def range_lookup(field, op, value):
239         return {"{}__{}".format(field_name(field), op): conv(value)}
240 
241     gte = "gt" if startexcl else "gte"
242     lte = "lt" if endexcl else "lte"
243 
244     if start is not None and end is not None:
245         return Q(**range_lookup(field, gte, start)) & Q(**range_lookup(field, lte, end))
246     if start is not None:
247         return Q(**range_lookup(field, gte, start))
248     return Q(**range_lookup(field, lte, end))
249 
250 
251 def has_sql(text):
252     if text == "plural":
253         return Q(source__contains=PLURAL_SEPARATOR)
254     if text == "suggestion":
255         return Q(has_suggestion=True)
256     if text == "comment":
257         return Q(has_comment=True)
258     if text in ("check", "failing-check", "failing_check"):
259         return Q(has_failing_check=True)
260     if text in ("ignored-check", "ignored_check"):
261         return Q(check__ignore=True)
262     if text == "translation":
263         return Q(state__gte=STATE_TRANSLATED)
264     if text == "shaping":
265         return Q(shaping__isnull=False)
266     if text == "label":
267         return Q(labels__isnull=False)
268 
269     raise ValueError("Unsupported has lookup: {}".format(text))
270 
271 
272 def is_sql(text):
273     if text in ("read-only", "readonly"):
274         return Q(state=STATE_READONLY)
275     if text == "approved":
276         return Q(state=STATE_APPROVED)
277     if text == "translated":
278         return Q(state__gte=STATE_TRANSLATED)
279     if text == "untranslated":
280         return Q(state__lt=STATE_TRANSLATED)
281     if text == "pending":
282         return Q(pending=True)
283 
284     raise ValueError("Unsupported is lookup: {}".format(text))
285 
286 
287 def exact_sql(field, text):
288     return Q(**{field_name(field, "iexact"): text})
289 
290 
291 def query_sql(obj):
292     if isinstance(obj, whoosh.query.And):
293         return reduce(
294             lambda x, y: x & y,
295             (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),
296         )
297     if isinstance(obj, whoosh.query.Or):
298         return reduce(
299             lambda x, y: x | y,
300             (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),
301         )
302     if isinstance(obj, whoosh.query.Not):
303         return ~query_sql(obj.query)
304     if isinstance(obj, Exact):
305         return exact_sql(obj.fieldname, obj.text)
306     if isinstance(obj, whoosh.query.Term):
307         if obj.fieldname == "has":
308             return has_sql(obj.text)
309         if obj.fieldname == "is":
310             return is_sql(obj.text)
311         return field_extra(obj.fieldname, Q(**{field_name(obj.fieldname): obj.text}))
312     if isinstance(obj, whoosh.query.DateRange):
313         return field_extra(
314             obj.fieldname,
315             range_sql(
316                 obj.fieldname,
317                 obj.startdate,
318                 obj.enddate,
319                 obj.startexcl,
320                 obj.endexcl,
321                 timezone.make_aware,
322             ),
323         )
324     if isinstance(obj, whoosh.query.NumericRange):
325         return range_sql(obj.fieldname, obj.start, obj.end, obj.startexcl, obj.endexcl)
326     if isinstance(obj, whoosh.query.Regex):
327         try:
328             re.compile(obj.text)
329             return Q(**{field_name(obj.fieldname, "regex"): obj.text})
330         except re.error as error:
331             raise ValueError(_("Invalid regular expression: {}").format(error))
332 
333     if obj == whoosh.query.NullQuery:
334         return Q()
335     raise ValueError("Unsupported: {!r}".format(obj))
336 
337 
338 @lru_cache(maxsize=512)
339 def parse_query(text):
340     return query_sql(PARSER.parse(text))
341
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/weblate/utils/search.py b/weblate/utils/search.py
--- a/weblate/utils/search.py
+++ b/weblate/utils/search.py
@@ -69,8 +69,8 @@
     pass
 
 
-class ExactPlugin(QuotePlugin):
-    """Exact match plugin with quotes to specify an exact term."""
+class ExactPlugin(whoosh.qparser.TaggingPlugin):
+    """Exact match plugin to specify an exact term."""
 
     class ExactNode(whoosh.qparser.syntax.TextNode):
         qclass = Exact
@@ -78,7 +78,7 @@
         def r(self):
             return "Exact %r" % self.text
 
-    expr = r"\=(^|(?<=\W))['\"](?P<text>.*?)['\"](?=\s|\]|[)}]|$)"
+    expr = r"\=(^|(?<=\W))(['\"]?)(?P<text>.*?)\2(?=\s|\]|[)}]|$)"
     nodetype = ExactNode

{"golden_diff": "diff --git a/weblate/utils/search.py b/weblate/utils/search.py\n--- a/weblate/utils/search.py\n+++ b/weblate/utils/search.py\n@@ -69,8 +69,8 @@\n     pass\n \n \n-class ExactPlugin(QuotePlugin):\n-    \"\"\"Exact match plugin with quotes to specify an exact term.\"\"\"\n+class ExactPlugin(whoosh.qparser.TaggingPlugin):\n+    \"\"\"Exact match plugin to specify an exact term.\"\"\"\n \n     class ExactNode(whoosh.qparser.syntax.TextNode):\n         qclass = Exact\n@@ -78,7 +78,7 @@\n         def r(self):\n             return \"Exact %r\" % self.text\n \n-    expr = r\"\\=(^|(?<=\\W))['\\\"](?P<text>.*?)['\\\"](?=\\s|\\]|[)}]|$)\"\n+    expr = r\"\\=(^|(?<=\\W))(['\\\"]?)(?P<text>.*?)\\2(?=\\s|\\]|[)}]|$)\"\n     nodetype = ExactNode\n", "issue": "Search / Criteria\"Exact\" overwrites 'Add\" button\nThe search is a great feature (quick to fill, powerfull  - with advanced criterias - and results are fast and reliable) but I have noticed an issue in the layout : \"Exact\" (which seems to be an untranslatable string yet) overwrite the \"Add\" button (at least in french, the full french label is \"Ajouter\"); see:  ![Search](https://user-images.githubusercontent.com/1452146/78504660-e0368a00-776e-11ea-9ede-ce77263e4107.jpg)\r\n\r\npage https://hosted.weblate.org/projects/weblate/master/#search \r\n\n", "before_files": [{"content": "#\n# Copyright \u00a9 2012 - 2020 Michal \u010ciha\u0159 <michal@cihar.com>\n#\n# This file is part of Weblate <https://weblate.org/>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <https://www.gnu.org/licenses/>.\n#\n\n\nimport re\nfrom functools import lru_cache, reduce\n\nimport whoosh.qparser\nimport whoosh.qparser.dateparse\nimport whoosh.query\nfrom django.db.models import Q\nfrom django.utils import timezone\nfrom django.utils.translation import gettext as _\nfrom jellyfish import damerau_levenshtein_distance\nfrom whoosh.fields import BOOLEAN, DATETIME, NUMERIC, TEXT, Schema\n\nfrom weblate.trans.util import PLURAL_SEPARATOR\nfrom weblate.utils.state import (\n    STATE_APPROVED,\n    STATE_NAMES,\n    STATE_READONLY,\n    STATE_TRANSLATED,\n)\n\n\nclass Comparer:\n    \"\"\"String comparer abstraction.\n\n    The reason is to be able to change implementation.\n    \"\"\"\n\n    def similarity(self, first, second):\n        \"\"\"Returns string similarity in range 0 - 100%.\"\"\"\n        try:\n            distance = damerau_levenshtein_distance(first, second)\n            return int(\n                100 * (1.0 - (float(distance) / max(len(first), len(second), 1)))\n            )\n        except MemoryError:\n            # Too long string, mark them as not much similar\n            return 50\n\n\nclass QuotePlugin(whoosh.qparser.SingleQuotePlugin):\n    \"\"\"Single and double quotes to specify a term.\"\"\"\n\n    expr = r\"(^|(?<=\\W))['\\\"](?P<text>.*?)['\\\"](?=\\s|\\]|[)}]|$)\"\n\n\nclass Exact(whoosh.query.Term):\n    \"\"\"Class for queries with exact operator.\"\"\"\n\n    pass\n\n\nclass ExactPlugin(QuotePlugin):\n    \"\"\"Exact match plugin with quotes to specify an exact term.\"\"\"\n\n    class ExactNode(whoosh.qparser.syntax.TextNode):\n        qclass = Exact\n\n        def r(self):\n            return \"Exact %r\" % self.text\n\n    expr = r\"\\=(^|(?<=\\W))['\\\"](?P<text>.*?)['\\\"](?=\\s|\\]|[)}]|$)\"\n    nodetype = ExactNode\n\n\nclass GtLtPlugin(whoosh.qparser.GtLtPlugin):\n    \"\"\"GtLt plugin taggin only after \":\".\"\"\"\n\n    def match(self, parser, text, pos):\n        if pos == 0 or text[pos - 1] != \":\":\n            return None\n        return super().match(parser, text, pos)\n\n\nclass DateParser(whoosh.qparser.dateparse.English):\n    def setup(self):\n        super().setup()\n        # We prefer simple parser prior to datetime\n        # This might not be necessary after following issue is fixed:\n        # https://github.com/whoosh-community/whoosh/issues/552\n        self.bundle.elements = (self.plusdate, self.simple, self.datetime)\n\n\nclass NumberField(NUMERIC):\n    def to_bytes(self, x, shift=0):\n        return int(x)\n\n\nclass StateField(NumberField):\n    def parse_query(self, fieldname, qstring, boost=1.0):\n        return super().parse_query(fieldname, state_to_int(qstring), boost)\n\n    def parse_range(self, fieldname, start, end, startexcl, endexcl, boost=1.0):\n        return super().parse_range(\n            fieldname, state_to_int(start), state_to_int(end), startexcl, endexcl, boost\n        )\n\n\ndef state_to_int(text):\n    if text is None:\n        return None\n    try:\n        return STATE_NAMES[text]\n    except KeyError:\n        raise ValueError(_(\"Unsupported state: {}\").format(text))\n\n\nclass QueryParser(whoosh.qparser.QueryParser):\n    \"\"\"Weblate query parser, differences to Whoosh default.\n\n    - no phrase plugin\n    - <> operators support\n    - double and single quotes behave identical\n    - multifield lookup for unspecified terms\n    \"\"\"\n\n    def __init__(self):\n        # Define fields for parsing\n        fields = {\n            # Unit fields\n            \"source\": TEXT,\n            \"target\": TEXT,\n            \"context\": TEXT,\n            \"note\": TEXT,\n            \"location\": TEXT,\n            \"priority\": NumberField,\n            \"added\": DATETIME,\n            \"state\": StateField,\n            \"pending\": BOOLEAN,\n            \"has\": TEXT,\n            \"is\": TEXT,\n            # Language\n            \"language\": TEXT,\n            # Change fields\n            \"changed\": DATETIME,\n            \"changed_by\": TEXT,\n            # Unit data\n            \"check\": TEXT,\n            \"ignored_check\": TEXT,\n            \"suggestion\": TEXT,\n            \"suggestion_author\": TEXT,\n            \"comment\": TEXT,\n            \"comment_author\": TEXT,\n            \"label\": TEXT,\n        }\n        schema = Schema(**fields)\n\n        # List of plugins\n        plugins = [\n            whoosh.qparser.WhitespacePlugin(),\n            QuotePlugin(),\n            whoosh.qparser.FieldsPlugin(),\n            whoosh.qparser.RangePlugin(),\n            GtLtPlugin(),\n            ExactPlugin(),\n            whoosh.qparser.RegexPlugin(),\n            whoosh.qparser.GroupPlugin(),\n            whoosh.qparser.OperatorsPlugin(),\n            whoosh.qparser.dateparse.DateParserPlugin(dateparser=DateParser()),\n            whoosh.qparser.MultifieldPlugin([\"source\", \"target\", \"context\"]),\n        ]\n        super().__init__(None, schema, plugins=plugins)\n\n    def term_query(\n        self, fieldname, text, termclass, boost=1.0, tokenize=True, removestops=True\n    ):\n        if self.schema and fieldname in self.schema:\n            if isinstance(self.schema[fieldname], TEXT):\n                return termclass(fieldname, text, boost=boost)\n        return super().term_query(\n            fieldname, text, termclass, boost, tokenize, removestops\n        )\n\n\nPARSER = QueryParser()\n\nPLAIN_FIELDS = (\"source\", \"target\", \"context\", \"note\", \"location\")\nFIELD_MAP = {\"changed\": \"change__timestamp\", \"added\": \"timestamp\"}\nSTRING_FIELD_MAP = {\"suggestion\": \"suggestion__target\", \"comment\": \"comment__comment\"}\nEXACT_FIELD_MAP = {\n    \"check\": \"check__check\",\n    \"ignored_check\": \"check__check\",\n    \"language\": \"translation__language__code\",\n    \"changed_by\": \"change__author__username\",\n    \"suggestion_author\": \"suggestion__user__username\",\n    \"comment_author\": \"comment__user__username\",\n    \"label\": \"labels__name\",\n}\n\n\ndef field_name(field, suffix=\"substring\"):\n    if field in FIELD_MAP:\n        return FIELD_MAP[field]\n    if field in PLAIN_FIELDS:\n        return \"{}__{}\".format(field, suffix)\n    if field in STRING_FIELD_MAP:\n        return \"{}__{}\".format(STRING_FIELD_MAP[field], suffix)\n    if field in EXACT_FIELD_MAP:\n        # Change contains to exact, do not change other (for example regex)\n        if suffix == \"substring\":\n            suffix = \"iexact\"\n        return \"{}__{}\".format(EXACT_FIELD_MAP[field], suffix)\n    return field\n\n\ndef field_extra(field, query):\n    from weblate.trans.models import Change\n\n    if field in {\"changed\", \"changed_by\"}:\n        return query & Q(change__action__in=Change.ACTIONS_CONTENT)\n    if field == \"check\":\n        return query & Q(check__ignore=False)\n    if field == \"ignored_check\":\n        return query & Q(check__ignore=True)\n    return query\n\n\ndef range_sql(field, start, end, startexcl, endexcl, conv=int):\n    def range_lookup(field, op, value):\n        return {\"{}__{}\".format(field_name(field), op): conv(value)}\n\n    gte = \"gt\" if startexcl else \"gte\"\n    lte = \"lt\" if endexcl else \"lte\"\n\n    if start is not None and end is not None:\n        return Q(**range_lookup(field, gte, start)) & Q(**range_lookup(field, lte, end))\n    if start is not None:\n        return Q(**range_lookup(field, gte, start))\n    return Q(**range_lookup(field, lte, end))\n\n\ndef has_sql(text):\n    if text == \"plural\":\n        return Q(source__contains=PLURAL_SEPARATOR)\n    if text == \"suggestion\":\n        return Q(has_suggestion=True)\n    if text == \"comment\":\n        return Q(has_comment=True)\n    if text in (\"check\", \"failing-check\", \"failing_check\"):\n        return Q(has_failing_check=True)\n    if text in (\"ignored-check\", \"ignored_check\"):\n        return Q(check__ignore=True)\n    if text == \"translation\":\n        return Q(state__gte=STATE_TRANSLATED)\n    if text == \"shaping\":\n        return Q(shaping__isnull=False)\n    if text == \"label\":\n        return Q(labels__isnull=False)\n\n    raise ValueError(\"Unsupported has lookup: {}\".format(text))\n\n\ndef is_sql(text):\n    if text in (\"read-only\", \"readonly\"):\n        return Q(state=STATE_READONLY)\n    if text == \"approved\":\n        return Q(state=STATE_APPROVED)\n    if text == \"translated\":\n        return Q(state__gte=STATE_TRANSLATED)\n    if text == \"untranslated\":\n        return Q(state__lt=STATE_TRANSLATED)\n    if text == \"pending\":\n        return Q(pending=True)\n\n    raise ValueError(\"Unsupported is lookup: {}\".format(text))\n\n\ndef exact_sql(field, text):\n    return Q(**{field_name(field, \"iexact\"): text})\n\n\ndef query_sql(obj):\n    if isinstance(obj, whoosh.query.And):\n        return reduce(\n            lambda x, y: x & y,\n            (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),\n        )\n    if isinstance(obj, whoosh.query.Or):\n        return reduce(\n            lambda x, y: x | y,\n            (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),\n        )\n    if isinstance(obj, whoosh.query.Not):\n        return ~query_sql(obj.query)\n    if isinstance(obj, Exact):\n        return exact_sql(obj.fieldname, obj.text)\n    if isinstance(obj, whoosh.query.Term):\n        if obj.fieldname == \"has\":\n            return has_sql(obj.text)\n        if obj.fieldname == \"is\":\n            return is_sql(obj.text)\n        return field_extra(obj.fieldname, Q(**{field_name(obj.fieldname): obj.text}))\n    if isinstance(obj, whoosh.query.DateRange):\n        return field_extra(\n            obj.fieldname,\n            range_sql(\n                obj.fieldname,\n                obj.startdate,\n                obj.enddate,\n                obj.startexcl,\n                obj.endexcl,\n                timezone.make_aware,\n            ),\n        )\n    if isinstance(obj, whoosh.query.NumericRange):\n        return range_sql(obj.fieldname, obj.start, obj.end, obj.startexcl, obj.endexcl)\n    if isinstance(obj, whoosh.query.Regex):\n        try:\n            re.compile(obj.text)\n            return Q(**{field_name(obj.fieldname, \"regex\"): obj.text})\n        except re.error as error:\n            raise ValueError(_(\"Invalid regular expression: {}\").format(error))\n\n    if obj == whoosh.query.NullQuery:\n        return Q()\n    raise ValueError(\"Unsupported: {!r}\".format(obj))\n\n\n@lru_cache(maxsize=512)\ndef parse_query(text):\n    return query_sql(PARSER.parse(text))\n", "path": "weblate/utils/search.py"}], "after_files": [{"content": "#\n# Copyright \u00a9 2012 - 2020 Michal \u010ciha\u0159 <michal@cihar.com>\n#\n# This file is part of Weblate <https://weblate.org/>\n#\n# This program is free software: you can redistribute it and/or modify\n# it under the terms of the GNU General Public License as published by\n# the Free Software Foundation, either version 3 of the License, or\n# (at your option) any later version.\n#\n# This program is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n# GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with this program.  If not, see <https://www.gnu.org/licenses/>.\n#\n\n\nimport re\nfrom functools import lru_cache, reduce\n\nimport whoosh.qparser\nimport whoosh.qparser.dateparse\nimport whoosh.query\nfrom django.db.models import Q\nfrom django.utils import timezone\nfrom django.utils.translation import gettext as _\nfrom jellyfish import damerau_levenshtein_distance\nfrom whoosh.fields import BOOLEAN, DATETIME, NUMERIC, TEXT, Schema\n\nfrom weblate.trans.util import PLURAL_SEPARATOR\nfrom weblate.utils.state import (\n    STATE_APPROVED,\n    STATE_NAMES,\n    STATE_READONLY,\n    STATE_TRANSLATED,\n)\n\n\nclass Comparer:\n    \"\"\"String comparer abstraction.\n\n    The reason is to be able to change implementation.\n    \"\"\"\n\n    def similarity(self, first, second):\n        \"\"\"Returns string similarity in range 0 - 100%.\"\"\"\n        try:\n            distance = damerau_levenshtein_distance(first, second)\n            return int(\n                100 * (1.0 - (float(distance) / max(len(first), len(second), 1)))\n            )\n        except MemoryError:\n            # Too long string, mark them as not much similar\n            return 50\n\n\nclass QuotePlugin(whoosh.qparser.SingleQuotePlugin):\n    \"\"\"Single and double quotes to specify a term.\"\"\"\n\n    expr = r\"(^|(?<=\\W))['\\\"](?P<text>.*?)['\\\"](?=\\s|\\]|[)}]|$)\"\n\n\nclass Exact(whoosh.query.Term):\n    \"\"\"Class for queries with exact operator.\"\"\"\n\n    pass\n\n\nclass ExactPlugin(whoosh.qparser.TaggingPlugin):\n    \"\"\"Exact match plugin to specify an exact term.\"\"\"\n\n    class ExactNode(whoosh.qparser.syntax.TextNode):\n        qclass = Exact\n\n        def r(self):\n            return \"Exact %r\" % self.text\n\n    expr = r\"\\=(^|(?<=\\W))(['\\\"]?)(?P<text>.*?)\\2(?=\\s|\\]|[)}]|$)\"\n    nodetype = ExactNode\n\n\nclass GtLtPlugin(whoosh.qparser.GtLtPlugin):\n    \"\"\"GtLt plugin taggin only after \":\".\"\"\"\n\n    def match(self, parser, text, pos):\n        if pos == 0 or text[pos - 1] != \":\":\n            return None\n        return super().match(parser, text, pos)\n\n\nclass DateParser(whoosh.qparser.dateparse.English):\n    def setup(self):\n        super().setup()\n        # We prefer simple parser prior to datetime\n        # This might not be necessary after following issue is fixed:\n        # https://github.com/whoosh-community/whoosh/issues/552\n        self.bundle.elements = (self.plusdate, self.simple, self.datetime)\n\n\nclass NumberField(NUMERIC):\n    def to_bytes(self, x, shift=0):\n        return int(x)\n\n\nclass StateField(NumberField):\n    def parse_query(self, fieldname, qstring, boost=1.0):\n        return super().parse_query(fieldname, state_to_int(qstring), boost)\n\n    def parse_range(self, fieldname, start, end, startexcl, endexcl, boost=1.0):\n        return super().parse_range(\n            fieldname, state_to_int(start), state_to_int(end), startexcl, endexcl, boost\n        )\n\n\ndef state_to_int(text):\n    if text is None:\n        return None\n    try:\n        return STATE_NAMES[text]\n    except KeyError:\n        raise ValueError(_(\"Unsupported state: {}\").format(text))\n\n\nclass QueryParser(whoosh.qparser.QueryParser):\n    \"\"\"Weblate query parser, differences to Whoosh default.\n\n    - no phrase plugin\n    - <> operators support\n    - double and single quotes behave identical\n    - multifield lookup for unspecified terms\n    \"\"\"\n\n    def __init__(self):\n        # Define fields for parsing\n        fields = {\n            # Unit fields\n            \"source\": TEXT,\n            \"target\": TEXT,\n            \"context\": TEXT,\n            \"note\": TEXT,\n            \"location\": TEXT,\n            \"priority\": NumberField,\n            \"added\": DATETIME,\n            \"state\": StateField,\n            \"pending\": BOOLEAN,\n            \"has\": TEXT,\n            \"is\": TEXT,\n            # Language\n            \"language\": TEXT,\n            # Change fields\n            \"changed\": DATETIME,\n            \"changed_by\": TEXT,\n            # Unit data\n            \"check\": TEXT,\n            \"ignored_check\": TEXT,\n            \"suggestion\": TEXT,\n            \"suggestion_author\": TEXT,\n            \"comment\": TEXT,\n            \"comment_author\": TEXT,\n            \"label\": TEXT,\n        }\n        schema = Schema(**fields)\n\n        # List of plugins\n        plugins = [\n            whoosh.qparser.WhitespacePlugin(),\n            QuotePlugin(),\n            whoosh.qparser.FieldsPlugin(),\n            whoosh.qparser.RangePlugin(),\n            GtLtPlugin(),\n            ExactPlugin(),\n            whoosh.qparser.RegexPlugin(),\n            whoosh.qparser.GroupPlugin(),\n            whoosh.qparser.OperatorsPlugin(),\n            whoosh.qparser.dateparse.DateParserPlugin(dateparser=DateParser()),\n            whoosh.qparser.MultifieldPlugin([\"source\", \"target\", \"context\"]),\n        ]\n        super().__init__(None, schema, plugins=plugins)\n\n    def term_query(\n        self, fieldname, text, termclass, boost=1.0, tokenize=True, removestops=True\n    ):\n        if self.schema and fieldname in self.schema:\n            if isinstance(self.schema[fieldname], TEXT):\n                return termclass(fieldname, text, boost=boost)\n        return super().term_query(\n            fieldname, text, termclass, boost, tokenize, removestops\n        )\n\n\nPARSER = QueryParser()\n\nPLAIN_FIELDS = (\"source\", \"target\", \"context\", \"note\", \"location\")\nFIELD_MAP = {\"changed\": \"change__timestamp\", \"added\": \"timestamp\"}\nSTRING_FIELD_MAP = {\"suggestion\": \"suggestion__target\", \"comment\": \"comment__comment\"}\nEXACT_FIELD_MAP = {\n    \"check\": \"check__check\",\n    \"ignored_check\": \"check__check\",\n    \"language\": \"translation__language__code\",\n    \"changed_by\": \"change__author__username\",\n    \"suggestion_author\": \"suggestion__user__username\",\n    \"comment_author\": \"comment__user__username\",\n    \"label\": \"labels__name\",\n}\n\n\ndef field_name(field, suffix=\"substring\"):\n    if field in FIELD_MAP:\n        return FIELD_MAP[field]\n    if field in PLAIN_FIELDS:\n        return \"{}__{}\".format(field, suffix)\n    if field in STRING_FIELD_MAP:\n        return \"{}__{}\".format(STRING_FIELD_MAP[field], suffix)\n    if field in EXACT_FIELD_MAP:\n        # Change contains to exact, do not change other (for example regex)\n        if suffix == \"substring\":\n            suffix = \"iexact\"\n        return \"{}__{}\".format(EXACT_FIELD_MAP[field], suffix)\n    return field\n\n\ndef field_extra(field, query):\n    from weblate.trans.models import Change\n\n    if field in {\"changed\", \"changed_by\"}:\n        return query & Q(change__action__in=Change.ACTIONS_CONTENT)\n    if field == \"check\":\n        return query & Q(check__ignore=False)\n    if field == \"ignored_check\":\n        return query & Q(check__ignore=True)\n    return query\n\n\ndef range_sql(field, start, end, startexcl, endexcl, conv=int):\n    def range_lookup(field, op, value):\n        return {\"{}__{}\".format(field_name(field), op): conv(value)}\n\n    gte = \"gt\" if startexcl else \"gte\"\n    lte = \"lt\" if endexcl else \"lte\"\n\n    if start is not None and end is not None:\n        return Q(**range_lookup(field, gte, start)) & Q(**range_lookup(field, lte, end))\n    if start is not None:\n        return Q(**range_lookup(field, gte, start))\n    return Q(**range_lookup(field, lte, end))\n\n\ndef has_sql(text):\n    if text == \"plural\":\n        return Q(source__contains=PLURAL_SEPARATOR)\n    if text == \"suggestion\":\n        return Q(has_suggestion=True)\n    if text == \"comment\":\n        return Q(has_comment=True)\n    if text in (\"check\", \"failing-check\", \"failing_check\"):\n        return Q(has_failing_check=True)\n    if text in (\"ignored-check\", \"ignored_check\"):\n        return Q(check__ignore=True)\n    if text == \"translation\":\n        return Q(state__gte=STATE_TRANSLATED)\n    if text == \"shaping\":\n        return Q(shaping__isnull=False)\n    if text == \"label\":\n        return Q(labels__isnull=False)\n\n    raise ValueError(\"Unsupported has lookup: {}\".format(text))\n\n\ndef is_sql(text):\n    if text in (\"read-only\", \"readonly\"):\n        return Q(state=STATE_READONLY)\n    if text == \"approved\":\n        return Q(state=STATE_APPROVED)\n    if text == \"translated\":\n        return Q(state__gte=STATE_TRANSLATED)\n    if text == \"untranslated\":\n        return Q(state__lt=STATE_TRANSLATED)\n    if text == \"pending\":\n        return Q(pending=True)\n\n    raise ValueError(\"Unsupported is lookup: {}\".format(text))\n\n\ndef exact_sql(field, text):\n    return Q(**{field_name(field, \"iexact\"): text})\n\n\ndef query_sql(obj):\n    if isinstance(obj, whoosh.query.And):\n        return reduce(\n            lambda x, y: x & y,\n            (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),\n        )\n    if isinstance(obj, whoosh.query.Or):\n        return reduce(\n            lambda x, y: x | y,\n            (query_sql(q) for q in obj.subqueries if q != whoosh.query.NullQuery),\n        )\n    if isinstance(obj, whoosh.query.Not):\n        return ~query_sql(obj.query)\n    if isinstance(obj, Exact):\n        return exact_sql(obj.fieldname, obj.text)\n    if isinstance(obj, whoosh.query.Term):\n        if obj.fieldname == \"has\":\n            return has_sql(obj.text)\n        if obj.fieldname == \"is\":\n            return is_sql(obj.text)\n        return field_extra(obj.fieldname, Q(**{field_name(obj.fieldname): obj.text}))\n    if isinstance(obj, whoosh.query.DateRange):\n        return field_extra(\n            obj.fieldname,\n            range_sql(\n                obj.fieldname,\n                obj.startdate,\n                obj.enddate,\n                obj.startexcl,\n                obj.endexcl,\n                timezone.make_aware,\n            ),\n        )\n    if isinstance(obj, whoosh.query.NumericRange):\n        return range_sql(obj.fieldname, obj.start, obj.end, obj.startexcl, obj.endexcl)\n    if isinstance(obj, whoosh.query.Regex):\n        try:\n            re.compile(obj.text)\n            return Q(**{field_name(obj.fieldname, \"regex\"): obj.text})\n        except re.error as error:\n            raise ValueError(_(\"Invalid regular expression: {}\").format(error))\n\n    if obj == whoosh.query.NullQuery:\n        return Q()\n    raise ValueError(\"Unsupported: {!r}\".format(obj))\n\n\n@lru_cache(maxsize=512)\ndef parse_query(text):\n    return query_sql(PARSER.parse(text))\n", "path": "weblate/utils/search.py"}]}