Skip to content

Commit

Permalink
Fixed #15819 - Fixed 1.3 regression from r15526 causing duplicate sea…
Browse files Browse the repository at this point in the history
…rch results in admin with search_fields traversing to non-M2M related models. Thanks to Adam Kochanowski for the report and Ryan Kaskel for the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16093 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
carljm committed Apr 23, 2011
1 parent 5bbba4b commit 065e6b6
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 18 deletions.
14 changes: 11 additions & 3 deletions django/contrib/admin/views/main.py
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
# Text to display within change-list table cells if the value is blank. # Text to display within change-list table cells if the value is blank.
EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)') EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')


def field_needs_distinct(field):
if ((hasattr(field, 'rel') and
isinstance(field.rel, models.ManyToManyRel)) or
(isinstance(field, models.related.RelatedObject) and
not field.field.unique)):
return True
return False


class ChangeList(object): class ChangeList(object):
def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin): def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
self.model = model self.model = model
Expand Down Expand Up @@ -189,8 +198,7 @@ def get_query_set(self):
f = self.lookup_opts.get_field_by_name(field_name)[0] f = self.lookup_opts.get_field_by_name(field_name)[0]
except models.FieldDoesNotExist: except models.FieldDoesNotExist:
raise IncorrectLookupParameters raise IncorrectLookupParameters
if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel): use_distinct = field_needs_distinct(f)
use_distinct = True


# if key ends with __in, split parameter into separate values # if key ends with __in, split parameter into separate values
if key.endswith('__in'): if key.endswith('__in'):
Expand Down Expand Up @@ -264,7 +272,7 @@ def construct_search(field_name):
for search_spec in orm_lookups: for search_spec in orm_lookups:
field_name = search_spec.split('__', 1)[0] field_name = search_spec.split('__', 1)[0]
f = self.lookup_opts.get_field_by_name(field_name)[0] f = self.lookup_opts.get_field_by_name(field_name)[0]
if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel): if field_needs_distinct(f):
use_distinct = True use_distinct = True
break break


Expand Down
84 changes: 69 additions & 15 deletions tests/regressiontests/admin_changelist/tests.py
Original file line number Original file line Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.views.main import ChangeList from django.contrib.admin.views.main import ChangeList, SEARCH_VAR
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.template import Context, Template from django.template import Context, Template
from django.test import TransactionTestCase from django.test import TransactionTestCase
Expand Down Expand Up @@ -148,12 +148,14 @@ def test_distinct_for_m2m_in_list_filter(self):
band.genres.add(blues) band.genres.add(blues)


m = BandAdmin(Band, admin.site) m = BandAdmin(Band, admin.site)
cl = ChangeList(MockFilteredRequestA(blues.pk), Band, m.list_display, request = MockFilterRequest('genres', blues.pk)

cl = ChangeList(request, Band, m.list_display,
m.list_display_links, m.list_filter, m.date_hierarchy, m.list_display_links, m.list_filter, m.date_hierarchy,
m.search_fields, m.list_select_related, m.list_per_page, m.search_fields, m.list_select_related, m.list_per_page,
m.list_editable, m) m.list_editable, m)


cl.get_results(MockFilteredRequestA(blues.pk)) cl.get_results(request)


# There's only one Group instance # There's only one Group instance
self.assertEqual(cl.result_count, 1) self.assertEqual(cl.result_count, 1)
Expand All @@ -169,12 +171,14 @@ def test_distinct_for_through_m2m_in_list_filter(self):
Membership.objects.create(group=band, music=lead, role='bass player') Membership.objects.create(group=band, music=lead, role='bass player')


m = GroupAdmin(Group, admin.site) m = GroupAdmin(Group, admin.site)
cl = ChangeList(MockFilteredRequestB(lead.pk), Group, m.list_display, request = MockFilterRequest('members', lead.pk)

cl = ChangeList(request, Group, m.list_display,
m.list_display_links, m.list_filter, m.date_hierarchy, m.list_display_links, m.list_filter, m.date_hierarchy,
m.search_fields, m.list_select_related, m.list_per_page, m.search_fields, m.list_select_related, m.list_per_page,
m.list_editable, m) m.list_editable, m)


cl.get_results(MockFilteredRequestB(lead.pk)) cl.get_results(request)


# There's only one Group instance # There's only one Group instance
self.assertEqual(cl.result_count, 1) self.assertEqual(cl.result_count, 1)
Expand All @@ -191,12 +195,14 @@ def test_distinct_for_inherited_m2m_in_list_filter(self):
Membership.objects.create(group=four, music=lead, role='guitar player') Membership.objects.create(group=four, music=lead, role='guitar player')


m = QuartetAdmin(Quartet, admin.site) m = QuartetAdmin(Quartet, admin.site)
cl = ChangeList(MockFilteredRequestB(lead.pk), Quartet, m.list_display, request = MockFilterRequest('members', lead.pk)

cl = ChangeList(request, Quartet, m.list_display,
m.list_display_links, m.list_filter, m.date_hierarchy, m.list_display_links, m.list_filter, m.date_hierarchy,
m.search_fields, m.list_select_related, m.list_per_page, m.search_fields, m.list_select_related, m.list_per_page,
m.list_editable, m) m.list_editable, m)


cl.get_results(MockFilteredRequestB(lead.pk)) cl.get_results(request)


# There's only one Quartet instance # There's only one Quartet instance
self.assertEqual(cl.result_count, 1) self.assertEqual(cl.result_count, 1)
Expand All @@ -213,16 +219,59 @@ def test_distinct_for_m2m_to_inherited_in_list_filter(self):
Invitation.objects.create(band=three, player=lead, instrument='bass') Invitation.objects.create(band=three, player=lead, instrument='bass')


m = ChordsBandAdmin(ChordsBand, admin.site) m = ChordsBandAdmin(ChordsBand, admin.site)
cl = ChangeList(MockFilteredRequestB(lead.pk), ChordsBand, m.list_display, request = MockFilterRequest('members', lead.pk)

cl = ChangeList(request, ChordsBand, m.list_display,
m.list_display_links, m.list_filter, m.date_hierarchy, m.list_display_links, m.list_filter, m.date_hierarchy,
m.search_fields, m.list_select_related, m.list_per_page, m.search_fields, m.list_select_related, m.list_per_page,
m.list_editable, m) m.list_editable, m)


cl.get_results(MockFilteredRequestB(lead.pk)) cl.get_results(request)


# There's only one ChordsBand instance # There's only one ChordsBand instance
self.assertEqual(cl.result_count, 1) self.assertEqual(cl.result_count, 1)


def test_distinct_for_non_unique_related_object_in_list_filter(self):
"""
Regressions tests for #15819: If a field listed in list_filters
is a non-unique related object, distinct() must be called.
"""
parent = Parent.objects.create(name='Mary')
# Two children with the same name
Child.objects.create(parent=parent, name='Daniel')
Child.objects.create(parent=parent, name='Daniel')

m = ParentAdmin(Parent, admin.site)
request = MockFilterRequest('child__name', 'Daniel')

cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
m.list_filter, m.date_hierarchy, m.search_fields,
m.list_select_related, m.list_per_page,
m.list_editable, m)

# Make sure distinct() was called
self.assertEqual(cl.query_set.count(), 1)

def test_distinct_for_non_unique_related_object_in_search_fields(self):
"""
Regressions tests for #15819: If a field listed in search_fields
is a non-unique related object, distinct() must be called.
"""
parent = Parent.objects.create(name='Mary')
Child.objects.create(parent=parent, name='Danielle')
Child.objects.create(parent=parent, name='Daniel')

m = ParentAdmin(Parent, admin.site)
request = MockSearchRequest('daniel')

cl = ChangeList(request, Parent, m.list_display, m.list_display_links,
m.list_filter, m.date_hierarchy, m.search_fields,
m.list_select_related, m.list_per_page,
m.list_editable, m)

# Make sure distinct() was called
self.assertEqual(cl.query_set.count(), 1)

def test_pagination(self): def test_pagination(self):
""" """
Regression tests for #12893: Pagination in admins changelist doesn't Regression tests for #12893: Pagination in admins changelist doesn't
Expand Down Expand Up @@ -254,6 +303,11 @@ def test_pagination(self):
self.assertEqual(cl.paginator.page_range, [1, 2, 3]) self.assertEqual(cl.paginator.page_range, [1, 2, 3])




class ParentAdmin(admin.ModelAdmin):
list_filter = ['child__name']
search_fields = ['child__name']


class ChildAdmin(admin.ModelAdmin): class ChildAdmin(admin.ModelAdmin):
list_display = ['name', 'parent'] list_display = ['name', 'parent']
list_per_page = 10 list_per_page = 10
Expand Down Expand Up @@ -288,10 +342,10 @@ class QuartetAdmin(admin.ModelAdmin):
class ChordsBandAdmin(admin.ModelAdmin): class ChordsBandAdmin(admin.ModelAdmin):
list_filter = ['members'] list_filter = ['members']


class MockFilteredRequestA(object): class MockFilterRequest(object):
def __init__(self, pk): def __init__(self, filter, q):
self.GET = { 'genres' : pk } self.GET = {filter: q}


class MockFilteredRequestB(object): class MockSearchRequest(object):
def __init__(self, pk): def __init__(self, q):
self.GET = { 'members': pk } self.GET = {SEARCH_VAR: q}

0 comments on commit 065e6b6

Please sign in to comment.