Fixed #15819 - Fixed 1.3 regression from r15526 causing duplicate search 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
This commit is contained in:
Carl Meyer 2011-04-23 03:55:21 +00:00
parent 5bbba4b9ad
commit 065e6b6153
2 changed files with 80 additions and 18 deletions

View File

@ -26,6 +26,15 @@ ERROR_FLAG = 'e'
# 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
@ -189,8 +198,7 @@ class ChangeList(object):
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'):
@ -264,7 +272,7 @@ class ChangeList(object):
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

View File

@ -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
@ -148,12 +148,14 @@ class ChangeListTests(TransactionTestCase):
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)
@ -169,12 +171,14 @@ class ChangeListTests(TransactionTestCase):
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)
@ -191,12 +195,14 @@ class ChangeListTests(TransactionTestCase):
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)
@ -213,16 +219,59 @@ class ChangeListTests(TransactionTestCase):
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
@ -254,6 +303,11 @@ class ChangeListTests(TransactionTestCase):
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
@ -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}