Skip to content

Commit

Permalink
Split search terms only for __contains queries
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
dbramwell and codingjoe committed Jan 9, 2021
1 parent 709ec19 commit 07054b2
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 9 deletions.
21 changes: 12 additions & 9 deletions django_select2/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
:parts: 1
"""
import re
import uuid
from functools import reduce
from itertools import chain
from pickle import PicklingError # nosec

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

_word_split_pattern = re.compile(r"\t|\n| ")

model = None
queryset = None
search_fields = []
Expand Down Expand Up @@ -397,14 +399,15 @@ def filter_queryset(self, request, term, queryset=None, **dependent_fields):
queryset = self.get_queryset()
search_fields = self.get_search_fields()
select = Q()
term = term.replace("\t", " ")
term = term.replace("\n", " ")
for t in [t for t in term.split(" ") if not t == ""]:
select &= reduce(
lambda x, y: x | Q(**{y: t}),
search_fields[1:],
Q(**{search_fields[0]: t}),
)

for field in search_fields:
field_select = Q(**{field: term})
if "contains" in field:
for word in filter(None, self._word_split_pattern.split(term)):
field_select |= Q(**{field: word})

select |= field_select

if dependent_fields:
select &= Q(**dependent_fields)

Expand Down
46 changes: 46 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,52 @@ def test_filter_queryset(self, genres):
)
assert qs.exists()

def test_filter_queryset__startswith(self, genres):
genre = Genre.objects.create(title="Space Genre")
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
assert widget.filter_queryset(None, genre.title).exists()

widget = TitleModelSelect2Widget(
search_fields=["title__istartswith"], queryset=Genre.objects.all()
)
qs = widget.filter_queryset(None, "Space Gen")
assert qs.exists()

qs = widget.filter_queryset(None, "Gen")
assert not qs.exists()

def test_filter_queryset__contains(self, genres):
genre = Genre.objects.create(title="Space Genre")
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
assert widget.filter_queryset(None, genre.title).exists()

widget = TitleModelSelect2Widget(
search_fields=["title__contains"], queryset=Genre.objects.all()
)
qs = widget.filter_queryset(None, "Space Gen")
assert qs.exists()

qs = widget.filter_queryset(None, "NOT Gen")
assert qs.exists(), "contains works even if only one part matches"

def test_filter_queryset__multiple_fields(self, genres):
genre = Genre.objects.create(title="Space Genre")
widget = TitleModelSelect2Widget(queryset=Genre.objects.all())
assert widget.filter_queryset(None, genre.title).exists()

widget = TitleModelSelect2Widget(
search_fields=[
"title__startswith",
"title__endswith",
],
queryset=Genre.objects.all(),
)
qs = widget.filter_queryset(None, "Space")
assert qs.exists()

qs = widget.filter_queryset(None, "Genre")
assert qs.exists()

def test_model_kwarg(self):
widget = ModelSelect2Widget(model=Genre, search_fields=["title__icontains"])
genre = Genre.objects.last()
Expand Down

0 comments on commit 07054b2

Please sign in to comment.