Skip to content

Commit

Permalink
Fixed django#6933 -- Added support for searching against quoted phras…
Browse files Browse the repository at this point in the history
…es in ModelAdmin.search_fields.
  • Loading branch information
alixleger authored and felixxm committed Jun 18, 2020
1 parent 8a902b7 commit 26a4135
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 8 deletions.
8 changes: 6 additions & 2 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@
from django.utils.html import format_html
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.text import capfirst, format_lazy, get_text_list
from django.utils.text import (
capfirst, format_lazy, get_text_list, smart_split, unescape_string_literal,
)
from django.utils.translation import gettext as _, ngettext
from django.views.decorators.csrf import csrf_protect
from django.views.generic import RedirectView
Expand Down Expand Up @@ -1022,7 +1024,9 @@ def construct_search(field_name):
if search_fields and search_term:
orm_lookups = [construct_search(str(search_field))
for search_field in search_fields]
for bit in search_term.split():
for bit in smart_split(search_term):
if bit.startswith(('"', "'")):
bit = unescape_string_literal(bit)
or_queries = [models.Q(**{orm_lookup: bit})
for orm_lookup in orm_lookups]
queryset = queryset.filter(reduce(operator.or_, or_queries))
Expand Down
16 changes: 12 additions & 4 deletions docs/ref/contrib/admin/index.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1309,14 +1309,18 @@ subclass::
WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%')
AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%')

The search query can contain quoted phrases with spaces. For example, if a
user searches for ``"john winston"`` or ``'john winston'``, Django will do
the equivalent of this SQL ``WHERE`` clause:

.. code-block:: sql

WHERE (first_name ILIKE '%john winston%' OR last_name ILIKE '%john winston%')

If you don't want to use ``icontains`` as the lookup, you can use any
lookup by appending it the field. For example, you could use :lookup:`exact`
by setting ``search_fields`` to ``['first_name__exact']``.

Beware that because query terms are split and ANDed as described earlier,
searching with :lookup:`exact` only works with a single search word since
two or more words can't all be an exact match unless all words are the same.

Some (older) shortcuts for specifying a field lookup are also available.
You can prefix a field in ``search_fields`` with the following characters
and it's equivalent to adding ``__<lookup>`` to the field:
Expand All @@ -1334,6 +1338,10 @@ subclass::
:meth:`ModelAdmin.get_search_results` to provide additional or alternate
search behavior.

.. versionchanged:: 3.2

Support for searching against quoted phrases with spaces was added.

.. attribute:: ModelAdmin.show_full_result_count

Set ``show_full_result_count`` to control whether the full count of objects
Expand Down
3 changes: 2 additions & 1 deletion docs/releases/3.2.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Minor features
:mod:`django.contrib.admin`
~~~~~~~~~~~~~~~~~~~~~~~~~~~

* ...
* :attr:`.ModelAdmin.search_fields` now allows searching against quoted phrases
with spaces.

:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
22 changes: 21 additions & 1 deletion tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3439,6 +3439,8 @@ def setUpTestData(cls):
cls.per1 = Person.objects.create(name='John Mauchly', gender=1, alive=True)
cls.per2 = Person.objects.create(name='Grace Hopper', gender=1, alive=False)
cls.per3 = Person.objects.create(name='Guido van Rossum', gender=1, alive=True)
Person.objects.create(name='John Doe', gender=1)
Person.objects.create(name="John O'Hara", gender=1)

cls.t1 = Recommender.objects.create()
cls.t2 = Recommendation.objects.create(the_recommender=cls.t1)
Expand Down Expand Up @@ -3513,7 +3515,7 @@ def test_reset_link(self):
response = self.client.get(reverse('admin:admin_views_person_changelist') + '?q=Gui')
self.assertContains(
response,
"""<span class="small quiet">1 result (<a href="?">3 total</a>)</span>""",
"""<span class="small quiet">1 result (<a href="?">5 total</a>)</span>""",
html=True
)

Expand All @@ -3533,6 +3535,24 @@ def test_no_total_count(self):
)
self.assertTrue(response.context['cl'].show_admin_actions)

def test_search_with_spaces(self):
url = reverse('admin:admin_views_person_changelist') + '?q=%s'
tests = [
('"John Doe"', 1),
("'John Doe'", 1),
('John Doe', 0),
('"John Doe" John', 1),
("'John Doe' John", 1),
("John Doe John", 0),
('"John Do"', 1),
("'John Do'", 1),
("'John O\\'Hara'", 1),
]
for search, hits in tests:
with self.subTest(search=search):
response = self.client.get(url % search)
self.assertContains(response, '\n%s person' % hits)


@override_settings(ROOT_URLCONF='admin_views.urls')
class AdminInheritedInlinesTest(TestCase):
Expand Down

0 comments on commit 26a4135

Please sign in to comment.