Skip to content

Commit 07054b2

Browse files
dbramwellcodingjoe
andcommitted
Split search terms only for __contains queries
Split search terms only for `__contains` queries and not for `__startswith` or `__endswith`. We no also split not only be whitespace but tab and newline. If multiple search fields are defined conditions combined with and OR. If single word matches in a for contains queries are OR combined as well. Co-Authored-By: codingjoe <[email protected]>
1 parent 709ec19 commit 07054b2

File tree

2 files changed

+58
-9
lines changed

2 files changed

+58
-9
lines changed

django_select2/forms.py

+12-9
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
:parts: 1
4747
4848
"""
49+
import re
4950
import uuid
50-
from functools import reduce
5151
from itertools import chain
5252
from pickle import PicklingError # nosec
5353

@@ -315,6 +315,8 @@ class HeavySelect2TagWidget(HeavySelect2Mixin, Select2TagWidget):
315315
class ModelSelect2Mixin:
316316
"""Widget mixin that provides attributes and methods for :class:`.AutoResponseView`."""
317317

318+
_word_split_pattern = re.compile(r"\t|\n| ")
319+
318320
model = None
319321
queryset = None
320322
search_fields = []
@@ -397,14 +399,15 @@ def filter_queryset(self, request, term, queryset=None, **dependent_fields):
397399
queryset = self.get_queryset()
398400
search_fields = self.get_search_fields()
399401
select = Q()
400-
term = term.replace("\t", " ")
401-
term = term.replace("\n", " ")
402-
for t in [t for t in term.split(" ") if not t == ""]:
403-
select &= reduce(
404-
lambda x, y: x | Q(**{y: t}),
405-
search_fields[1:],
406-
Q(**{search_fields[0]: t}),
407-
)
402+
403+
for field in search_fields:
404+
field_select = Q(**{field: term})
405+
if "contains" in field:
406+
for word in filter(None, self._word_split_pattern.split(term)):
407+
field_select |= Q(**{field: word})
408+
409+
select |= field_select
410+
408411
if dependent_fields:
409412
select &= Q(**dependent_fields)
410413

tests/test_forms.py

+46
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,52 @@ def test_filter_queryset(self, genres):
446446
)
447447
assert qs.exists()
448448

449+
def test_filter_queryset__startswith(self, genres):
450+
genre = Genre.objects.create(title="Space Genre")
451+
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
452+
assert widget.filter_queryset(None, genre.title).exists()
453+
454+
widget = TitleModelSelect2Widget(
455+
search_fields=["title__istartswith"], queryset=Genre.objects.all()
456+
)
457+
qs = widget.filter_queryset(None, "Space Gen")
458+
assert qs.exists()
459+
460+
qs = widget.filter_queryset(None, "Gen")
461+
assert not qs.exists()
462+
463+
def test_filter_queryset__contains(self, genres):
464+
genre = Genre.objects.create(title="Space Genre")
465+
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
466+
assert widget.filter_queryset(None, genre.title).exists()
467+
468+
widget = TitleModelSelect2Widget(
469+
search_fields=["title__contains"], queryset=Genre.objects.all()
470+
)
471+
qs = widget.filter_queryset(None, "Space Gen")
472+
assert qs.exists()
473+
474+
qs = widget.filter_queryset(None, "NOT Gen")
475+
assert qs.exists(), "contains works even if only one part matches"
476+
477+
def test_filter_queryset__multiple_fields(self, genres):
478+
genre = Genre.objects.create(title="Space Genre")
479+
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
480+
assert widget.filter_queryset(None, genre.title).exists()
481+
482+
widget = TitleModelSelect2Widget(
483+
search_fields=[
484+
"title__startswith",
485+
"title__endswith",
486+
],
487+
queryset=Genre.objects.all(),
488+
)
489+
qs = widget.filter_queryset(None, "Space")
490+
assert qs.exists()
491+
492+
qs = widget.filter_queryset(None, "Genre")
493+
assert qs.exists()
494+
449495
def test_model_kwarg(self):
450496
widget = ModelSelect2Widget(model=Genre, search_fields=["title__icontains"])
451497
genre = Genre.objects.last()

0 commit comments

Comments
 (0)