diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 8c09c10..2fa9c96 100644
a
|
b
|
class ChangeList(object):
|
164 | 164 | return order_field, order_type |
165 | 165 | |
166 | 166 | def get_query_set(self): |
| 167 | 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): |
170 | 172 | if i in lookup_params: |
171 | 173 | del lookup_params[i] |
172 | 174 | for key, value in lookup_params.items(): |
| 175 | if not DISTINCT and key.count('__') > 1: |
| 176 | # Check if it's a relationship that might return more than one |
| 177 | # instance |
| 178 | DISTINCT = True |
173 | 179 | if not isinstance(key, str): |
174 | 180 | # 'key' will be used as a keyword argument later, so Python |
175 | 181 | # requires it to be a string. |
… |
… |
class ChangeList(object):
|
230 | 236 | else: |
231 | 237 | return "%s__icontains" % field_name |
232 | 238 | |
233 | | if self.search_fields and self.query: |
| 239 | if not DISTINCT and self.search_fields and self.query: |
234 | 240 | for bit in self.query.split(): |
235 | 241 | or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields] |
236 | 242 | qs = qs.filter(reduce(operator.or_, or_queries)) |
237 | 243 | for field_name in self.search_fields: |
238 | 244 | if '__' in field_name: |
239 | | qs = qs.distinct() |
| 245 | DISCTINCT = True |
240 | 246 | break |
241 | 247 | |
242 | | return qs |
| 248 | if DISTINCT: |
| 249 | return qs.distinct() |
| 250 | else: |
| 251 | return qs |
243 | 252 | |
244 | 253 | def url_for_result(self, result): |
245 | 254 | 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 b70d7c5..569bfe8 100644
a
|
b
|
import unittest
|
2 | 2 | from django.contrib import admin |
3 | 3 | from django.contrib.admin.views.main import ChangeList |
4 | 4 | from django.template import Context, Template |
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(unittest.TestCase): |
| 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 | |
| 12 | def tearDown(self): |
| 13 | self.new_parent.delete() |
| 14 | self.new_child.delete() |
| 15 | |
8 | 16 | def test_select_related_preserved(self): |
9 | 17 | """ |
10 | 18 | Regression test for #10348: ChangeList.get_query_set() shouldn't |
… |
… |
class ChangeListTests(unittest.TestCase):
|
22 | 30 | table and this checks that the items are nested within the table |
23 | 31 | element tags. |
24 | 32 | """ |
25 | | new_parent = Parent.objects.create(name='parent') |
26 | | new_child = Child.objects.create(name='name', parent=new_parent) |
| 33 | new_parent = self.new_parent |
| 34 | new_child = self.new_child |
27 | 35 | request = MockRequest() |
28 | 36 | m = ChildAdmin(Child, admin.site) |
29 | 37 | cl = ChangeList(request, Child, m.list_display, m.list_display_links, |
… |
… |
class ChangeListTests(unittest.TestCase):
|
57 | 65 | self.failIf(table_output.find('<td>%s</td>' % hidden_input_elem) == -1, |
58 | 66 | 'Hidden input element is not enclosed in <td> element.') |
59 | 67 | |
| 68 | def test_distinct(self): |
| 69 | """ |
| 70 | Regression test for #13902: When using a ManyToMany in list_filter, |
| 71 | results may apper more than once |
| 72 | """ |
| 73 | new_parent = self.new_parent |
| 74 | new_child = self.new_child |
| 75 | relation1 = Through.objects.create(parent=new_parent, child=new_child) |
| 76 | relation2 = Through.objects.create(parent=new_parent, child=new_child) |
| 77 | |
| 78 | m = ChildAdmin(Child, admin.site) |
| 79 | cl = ChangeList(MockFilteredRequest(), Child, m.list_display, m.list_display_links, |
| 80 | m.list_filter, m.date_hierarchy, m.search_fields, |
| 81 | m.list_select_related, m.list_per_page, m.list_editable, m) |
| 82 | |
| 83 | cl.get_results(MockFilteredRequest()) |
| 84 | |
| 85 | # There's only one Child instance |
| 86 | self.assertEqual(cl.result_count, 1) |
| 87 | |
60 | 88 | class ChildAdmin(admin.ModelAdmin): |
61 | 89 | list_display = ['name', 'parent'] |
62 | 90 | def queryset(self, request): |
63 | 91 | return super(ChildAdmin, self).queryset(request).select_related("parent__name") |
| 92 | list_filter = ['multiple_m2m', ] |
64 | 93 | |
65 | 94 | class MockRequest(object): |
66 | 95 | GET = {} |
| 96 | |
| 97 | class MockFilteredRequest(object): |
| 98 | GET = {'multiple_m2m__id__exact': 1, } |
| 99 | |