Skip to content

Commit

Permalink
Fixed #16063 -- Matched all terms in admin changelist searches spanni…
Browse files Browse the repository at this point in the history
…ng multi-valued relationships.

Reduces the likelihood of admin searches issuing queries with excessive
joins.
  • Loading branch information
jacobtylerwalls committed Oct 25, 2021
1 parent 551c997 commit 0058864
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 4 deletions.
4 changes: 3 additions & 1 deletion django/contrib/admin/options.py
Expand Up @@ -1031,14 +1031,16 @@ def construct_search(field_name):
if search_fields and search_term:
orm_lookups = [construct_search(str(search_field))
for search_field in search_fields]
term_queries = []
for bit in smart_split(search_term):
if bit.startswith(('"', "'")) and bit[0] == bit[-1]:
bit = unescape_string_literal(bit)
or_queries = models.Q(
*((orm_lookup, bit) for orm_lookup in orm_lookups),
_connector=models.Q.OR,
)
queryset = queryset.filter(or_queries)
term_queries.append(or_queries)
queryset = queryset.filter(models.Q(*term_queries))
may_have_duplicates |= any(
lookup_spawns_duplicates(self.opts, search_spec)
for search_spec in orm_lookups
Expand Down
5 changes: 4 additions & 1 deletion docs/releases/4.1.txt
Expand Up @@ -247,7 +247,10 @@ Upstream support for MariaDB 10.2 ends in May 2022. Django 4.1 supports MariaDB
Miscellaneous
-------------

* ...
* Admin changelist searches employing multiple search terms over a single
reverse foreign key or many-to-many relationship now only return rows
from the related model where every search term matches its filter, following
the first example query in :ref:`topic-spanning-multi-valued-relationships`.

.. _deprecated-features-4.1:

Expand Down
2 changes: 2 additions & 0 deletions docs/topics/db/queries.txt
Expand Up @@ -525,6 +525,8 @@ those latter objects, you could write::

Blog.objects.filter(entry__authors__isnull=False, entry__authors__name__isnull=True)

.. _topic-spanning-multi-valued-relationships:

Spanning multi-valued relationships
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
6 changes: 6 additions & 0 deletions tests/admin_changelist/admin.py
Expand Up @@ -36,6 +36,12 @@ class ParentAdmin(admin.ModelAdmin):
list_select_related = ['child']


class ParentAdminTwoSearchFields(admin.ModelAdmin):
list_filter = ['child__name']
search_fields = ['child__name', 'child__age']
list_select_related = ['child']


class ChildAdmin(admin.ModelAdmin):
list_display = ['name', 'parent']
list_per_page = 10
Expand Down
33 changes: 31 additions & 2 deletions tests/admin_changelist/tests.py
Expand Up @@ -30,8 +30,8 @@
DynamicListDisplayLinksChildAdmin, DynamicListFilterChildAdmin,
DynamicSearchFieldsChildAdmin, EmptyValueChildAdmin, EventAdmin,
FilteredChildAdmin, GroupAdmin, InvitationAdmin,
NoListDisplayLinksParentAdmin, ParentAdmin, QuartetAdmin, SwallowAdmin,
site as custom_site,
NoListDisplayLinksParentAdmin, ParentAdmin, ParentAdminTwoSearchFields,
QuartetAdmin, SwallowAdmin, site as custom_site,
)
from .models import (
Band, CharPK, Child, ChordsBand, ChordsMusician, Concert, CustomIdUser,
Expand Down Expand Up @@ -153,6 +153,35 @@ def get_list_select_related(self, request):
cl = ia.get_changelist_instance(request)
self.assertEqual(cl.queryset.query.select_related, {'player': {}, 'band': {}})

def test_many_search_terms(self):
parent = Parent.objects.create(name='Mary')
Child.objects.create(parent=parent, name='Danielle')
Child.objects.create(parent=parent, name='Daniel')

m = ParentAdmin(Parent, custom_site)
request = self.factory.get('/parent/', data={SEARCH_VAR: 'daniel ' * 80})
request.user = self.superuser

cl = m.get_changelist_instance(request)
# Before: django.db.utils.OperationalError: at most 64 tables in a join
self.assertEqual(cl.queryset.count(), 1)

def test_related_field_multiple_search_terms(self):
"""
Searches over multi-valued relationships now return rows from
related models only when all searched fields match that row.
"""
parent = Parent.objects.create(name='Mary')
Child.objects.create(parent=parent, name='Danielle', age=18)
Child.objects.create(parent=parent, name='Daniel', age=19)

m = ParentAdminTwoSearchFields(Parent, custom_site)
request = self.factory.get('/parent/', data={SEARCH_VAR: 'danielle 19'})
request.user = self.superuser

cl = m.get_changelist_instance(request)
self.assertEqual(cl.queryset.count(), 0)

def test_result_list_empty_changelist_value(self):
"""
Regression test for #14982: EMPTY_CHANGELIST_VALUE should be honored
Expand Down

0 comments on commit 0058864

Please sign in to comment.