Fixed #6933 -- Added support for searching against quoted phrases in ModelAdmin.search_fields.
This commit is contained in:
parent
8a902b7ee6
commit
26a413507a
|
@ -44,7 +44,9 @@ from django.utils.decorators import method_decorator
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
from django.utils.safestring import mark_safe
|
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.utils.translation import gettext as _, ngettext
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
@ -1022,7 +1024,9 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
if search_fields and search_term:
|
if search_fields and search_term:
|
||||||
orm_lookups = [construct_search(str(search_field))
|
orm_lookups = [construct_search(str(search_field))
|
||||||
for search_field in search_fields]
|
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})
|
or_queries = [models.Q(**{orm_lookup: bit})
|
||||||
for orm_lookup in orm_lookups]
|
for orm_lookup in orm_lookups]
|
||||||
queryset = queryset.filter(reduce(operator.or_, or_queries))
|
queryset = queryset.filter(reduce(operator.or_, or_queries))
|
||||||
|
|
|
@ -1309,14 +1309,18 @@ subclass::
|
||||||
WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%')
|
WHERE (first_name ILIKE '%john%' OR last_name ILIKE '%john%')
|
||||||
AND (first_name ILIKE '%lennon%' OR last_name ILIKE '%lennon%')
|
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
|
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`
|
lookup by appending it the field. For example, you could use :lookup:`exact`
|
||||||
by setting ``search_fields`` to ``['first_name__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.
|
Some (older) shortcuts for specifying a field lookup are also available.
|
||||||
You can prefix a field in ``search_fields`` with the following characters
|
You can prefix a field in ``search_fields`` with the following characters
|
||||||
and it's equivalent to adding ``__<lookup>`` to the field:
|
and it's equivalent to adding ``__<lookup>`` to the field:
|
||||||
|
@ -1334,6 +1338,10 @@ subclass::
|
||||||
:meth:`ModelAdmin.get_search_results` to provide additional or alternate
|
:meth:`ModelAdmin.get_search_results` to provide additional or alternate
|
||||||
search behavior.
|
search behavior.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2
|
||||||
|
|
||||||
|
Support for searching against quoted phrases with spaces was added.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.show_full_result_count
|
.. attribute:: ModelAdmin.show_full_result_count
|
||||||
|
|
||||||
Set ``show_full_result_count`` to control whether the full count of objects
|
Set ``show_full_result_count`` to control whether the full count of objects
|
||||||
|
|
|
@ -37,7 +37,8 @@ Minor features
|
||||||
:mod:`django.contrib.admin`
|
:mod:`django.contrib.admin`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* ...
|
* :attr:`.ModelAdmin.search_fields` now allows searching against quoted phrases
|
||||||
|
with spaces.
|
||||||
|
|
||||||
:mod:`django.contrib.admindocs`
|
:mod:`django.contrib.admindocs`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
|
@ -3439,6 +3439,8 @@ class AdminSearchTest(TestCase):
|
||||||
cls.per1 = Person.objects.create(name='John Mauchly', gender=1, alive=True)
|
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.per2 = Person.objects.create(name='Grace Hopper', gender=1, alive=False)
|
||||||
cls.per3 = Person.objects.create(name='Guido van Rossum', gender=1, alive=True)
|
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.t1 = Recommender.objects.create()
|
||||||
cls.t2 = Recommendation.objects.create(the_recommender=cls.t1)
|
cls.t2 = Recommendation.objects.create(the_recommender=cls.t1)
|
||||||
|
@ -3513,7 +3515,7 @@ class AdminSearchTest(TestCase):
|
||||||
response = self.client.get(reverse('admin:admin_views_person_changelist') + '?q=Gui')
|
response = self.client.get(reverse('admin:admin_views_person_changelist') + '?q=Gui')
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
response,
|
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
|
html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3533,6 +3535,24 @@ class AdminSearchTest(TestCase):
|
||||||
)
|
)
|
||||||
self.assertTrue(response.context['cl'].show_admin_actions)
|
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')
|
@override_settings(ROOT_URLCONF='admin_views.urls')
|
||||||
class AdminInheritedInlinesTest(TestCase):
|
class AdminInheritedInlinesTest(TestCase):
|
||||||
|
|
Loading…
Reference in New Issue