diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 8c09c10..0c5886d 100644
a
|
b
|
class ChangeList(object):
|
164 | 164 | return order_field, order_type |
165 | 165 | |
166 | 166 | def get_query_set(self): |
| 167 | is_distinct = False |
| 168 | |
167 | 169 | qs = self.root_query_set |
168 | 170 | lookup_params = self.params.copy() # a dictionary of the query string |
169 | 171 | for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): |
… |
… |
class ChangeList(object):
|
176 | 178 | del lookup_params[key] |
177 | 179 | lookup_params[smart_str(key)] = value |
178 | 180 | |
| 181 | if not is_distinct: |
| 182 | # Check if it's a relationship that might return more than one |
| 183 | # instance |
| 184 | try: |
| 185 | f = self.lookup_opts.get_field(key.split('__',1)[0]) |
| 186 | except models.FieldDoesNotExist: |
| 187 | raise IncorrectLookupParameters |
| 188 | if isinstance(f.rel, (models.ManyToOneRel, models.ManyToManyRel)): |
| 189 | is_distinct = True |
| 190 | |
179 | 191 | # if key ends with __in, split parameter into separate values |
180 | 192 | if key.endswith('__in'): |
181 | 193 | lookup_params[key] = value.split(',') |
… |
… |
class ChangeList(object):
|
234 | 246 | for bit in self.query.split(): |
235 | 247 | or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields] |
236 | 248 | qs = qs.filter(reduce(operator.or_, or_queries)) |
237 | | for field_name in self.search_fields: |
238 | | if '__' in field_name: |
239 | | qs = qs.distinct() |
240 | | break |
| 249 | if not is_distinct: |
| 250 | for field_name in self.search_fields: |
| 251 | f = self.lookup_opts.get_field(field_name.split('__',1)[0]) |
| 252 | if isinstance(f.rel, (models.ManyToOneRel, models.ManyToManyRel)): |
| 253 | is_distinct = True |
| 254 | break |
241 | 255 | |
242 | | return qs |
| 256 | if is_distinct: |
| 257 | return qs.distinct() |
| 258 | else: |
| 259 | return qs |
243 | 260 | |
244 | 261 | def url_for_result(self, result): |
245 | 262 | return "%s/" % quote(getattr(result, self.pk_attname)) |
diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py
index f030a78..c043c8a 100644
a
|
b
|
class Parent(models.Model):
|
6 | 6 | |
7 | 7 | class Child(models.Model): |
8 | 8 | parent = models.ForeignKey(Parent, editable=False) |
9 | | name = models.CharField(max_length=30, blank=True) |
10 | | No newline at end of file |
| 9 | name = models.CharField(max_length=30, blank=True) |
| 10 | multiple_m2m = models.ManyToManyField(Parent, related_name='multiple_m2m', |
| 11 | through='Through') |
| 12 | |
| 13 | class Through(models.Model): |
| 14 | parent = models.ForeignKey(Parent) |
| 15 | child = models.ForeignKey(Child) |
diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
index c8ad1ce..89a3424 100644
a
|
b
|
from django.contrib import admin
|
2 | 2 | from django.contrib.admin.views.main import ChangeList |
3 | 3 | from django.template import Context, Template |
4 | 4 | from django.test import TransactionTestCase |
5 | | from regressiontests.admin_changelist.models import Child, Parent |
| 5 | from regressiontests.admin_changelist.models import Child, Parent, Through |
6 | 6 | |
7 | 7 | class ChangeListTests(TransactionTestCase): |
| 8 | def setUp(self): |
| 9 | self.new_parent = Parent.objects.create(name='parent') |
| 10 | self.new_child = Child.objects.create(name='name', parent=self.new_parent) |
| 11 | |
8 | 12 | def test_select_related_preserved(self): |
9 | 13 | """ |
10 | 14 | Regression test for #10348: ChangeList.get_query_set() shouldn't |
… |
… |
class ChangeListTests(TransactionTestCase):
|
21 | 25 | Verifies that inclusion tag result_list generates a table when with |
22 | 26 | default ModelAdmin settings. |
23 | 27 | """ |
24 | | new_parent = Parent.objects.create(name='parent') |
25 | | new_child = Child.objects.create(name='name', parent=new_parent) |
| 28 | new_parent = self.new_parent |
| 29 | new_child = self.new_child |
26 | 30 | request = MockRequest() |
27 | 31 | m = ChildAdmin(Child, admin.site) |
28 | 32 | cl = ChangeList(request, Child, m.list_display, m.list_display_links, |
… |
… |
class ChangeListTests(TransactionTestCase):
|
45 | 49 | when list_editable is enabled are rendered in a div outside the |
46 | 50 | table. |
47 | 51 | """ |
48 | | new_parent = Parent.objects.create(name='parent') |
49 | | new_child = Child.objects.create(name='name', parent=new_parent) |
| 52 | new_parent = self.new_parent |
| 53 | new_child = self.new_child |
50 | 54 | request = MockRequest() |
51 | 55 | m = ChildAdmin(Child, admin.site) |
52 | 56 | |
… |
… |
class ChangeListTests(TransactionTestCase):
|
71 | 75 | self.failIf('<td>%s</td>' % editable_name_field == -1, |
72 | 76 | 'Failed to find "name" list_editable field in: %s' % table_output) |
73 | 77 | |
| 78 | def test_distinct(self): |
| 79 | """ |
| 80 | Regression test for #13902: When using a ManyToMany in list_filter, |
| 81 | results may apper more than once |
| 82 | """ |
| 83 | new_parent = self.new_parent |
| 84 | new_child = self.new_child |
| 85 | relation1 = Through.objects.create(parent=new_parent, child=new_child) |
| 86 | relation2 = Through.objects.create(parent=new_parent, child=new_child) |
| 87 | |
| 88 | m = ChildAdmin(Child, admin.site) |
| 89 | cl = ChangeList(MockFilteredRequest(), Child, m.list_display, m.list_display_links, |
| 90 | m.list_filter, m.date_hierarchy, m.search_fields, |
| 91 | m.list_select_related, m.list_per_page, m.list_editable, m) |
| 92 | |
| 93 | cl.get_results(MockFilteredRequest()) |
| 94 | |
| 95 | # There's only one Child instance |
| 96 | self.assertEqual(cl.result_count, 1) |
| 97 | |
74 | 98 | class ChildAdmin(admin.ModelAdmin): |
75 | 99 | list_display = ['name', 'parent'] |
76 | 100 | def queryset(self, request): |
77 | 101 | return super(ChildAdmin, self).queryset(request).select_related("parent__name") |
| 102 | list_filter = ['multiple_m2m', ] |
78 | 103 | |
79 | 104 | class MockRequest(object): |
80 | 105 | GET = {} |
| 106 | |
| 107 | class MockFilteredRequest(object): |
| 108 | GET = {'multiple_m2m__id__exact': 1, } |
| 109 | |