diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 886ccd9..1ae1715 100644
a
|
b
|
class ChangeList(object):
|
169 | 169 | return order_field, order_type |
170 | 170 | |
171 | 171 | def get_query_set(self): |
| 172 | is_distinct = False |
| 173 | |
172 | 174 | qs = self.root_query_set |
173 | 175 | lookup_params = self.params.copy() # a dictionary of the query string |
174 | 176 | for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR): |
… |
… |
class ChangeList(object):
|
181 | 183 | del lookup_params[key] |
182 | 184 | lookup_params[smart_str(key)] = value |
183 | 185 | |
| 186 | if not is_distinct: |
| 187 | # Check if it's a relationship that might return more than one |
| 188 | # instance |
| 189 | try: |
| 190 | f = self.lookup_opts.get_field_by_name(key.split('__',1)[0]) |
| 191 | except models.FieldDoesNotExist: |
| 192 | raise IncorrectLookupParameters |
| 193 | if (isinstance(f[0], models.related.RelatedObject) or |
| 194 | isinstance(f[0].rel, (models.ManyToOneRel, |
| 195 | models.ManyToManyRel))): |
| 196 | is_distinct = True |
| 197 | |
184 | 198 | # if key ends with __in, split parameter into separate values |
185 | 199 | if key.endswith('__in'): |
186 | 200 | lookup_params[key] = value.split(',') |
… |
… |
class ChangeList(object):
|
244 | 258 | for bit in self.query.split(): |
245 | 259 | or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields] |
246 | 260 | qs = qs.filter(reduce(operator.or_, or_queries)) |
247 | | for field_name in self.search_fields: |
248 | | if '__' in field_name: |
249 | | qs = qs.distinct() |
250 | | break |
| 261 | if not is_distinct: |
| 262 | for field_name in self.search_fields: |
| 263 | f = self.lookup_opts.get_field_by_name(field_name.split('__',1)[0]) |
| 264 | if (isinstance(f[0], models.related.RelatedObject) or |
| 265 | isinstance(f[0].rel, (models.ManyToOneRel, |
| 266 | models.ManyToManyRel))): |
| 267 | is_distinct = True |
| 268 | break |
251 | 269 | |
252 | | return qs |
| 270 | if is_distinct: |
| 271 | return qs.distinct() |
| 272 | else: |
| 273 | return qs |
253 | 274 | |
254 | 275 | def url_for_result(self, result): |
255 | 276 | 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 858d6df..6944c78 100644
a
|
b
|
class Parent(models.Model):
|
7 | 7 | class Child(models.Model): |
8 | 8 | parent = models.ForeignKey(Parent, editable=False, null=True) |
9 | 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 c3f6186..cf8a154 100644
a
|
b
|
from django.core.paginator import Paginator
|
5 | 5 | from django.template import Context, Template |
6 | 6 | from django.test import TransactionTestCase |
7 | 7 | |
8 | | from regressiontests.admin_changelist.models import Child, Parent |
| 8 | from regressiontests.admin_changelist.models import Child, Parent, Through |
9 | 9 | |
10 | 10 | |
11 | 11 | class ChangeListTests(TransactionTestCase): |
| 12 | def setUp(self): |
| 13 | self.new_parent = Parent.objects.create(name='parent') |
| 14 | self.new_child = Child.objects.create(name='name', parent=self.new_parent) |
| 15 | |
12 | 16 | def test_select_related_preserved(self): |
13 | 17 | """ |
14 | 18 | Regression test for #10348: ChangeList.get_query_set() shouldn't |
… |
… |
class ChangeListTests(TransactionTestCase):
|
35 | 39 | template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}') |
36 | 40 | context = Context({'cl': cl}) |
37 | 41 | table_output = template.render(context) |
38 | | row_html = '<tbody><tr class="row1"><td><input type="checkbox" class="action-select" value="1" name="_selected_action" /></td><th><a href="1/">name</a></th><td>(None)</td></tr></tbody>' |
| 42 | row_html = '<tbody><tr class="row1"><td><input type="checkbox" class="action-select" value="2" name="_selected_action" /></td><th><a href="2/">name</a></th><td>(None)</td></tr>' |
39 | 43 | self.assertFalse(table_output.find(row_html) == -1, |
40 | 44 | 'Failed to find expected row element: %s' % table_output) |
41 | 45 | |
… |
… |
class ChangeListTests(TransactionTestCase):
|
45 | 49 | Verifies that inclusion tag result_list generates a table when with |
46 | 50 | default ModelAdmin settings. |
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 | cl = ChangeList(request, Child, m.list_display, m.list_display_links, |
… |
… |
class ChangeListTests(TransactionTestCase):
|
69 | 73 | when list_editable is enabled are rendered in a div outside the |
70 | 74 | table. |
71 | 75 | """ |
72 | | new_parent = Parent.objects.create(name='parent') |
73 | | new_child = Child.objects.create(name='name', parent=new_parent) |
| 76 | new_parent = self.new_parent |
| 77 | new_child = self.new_child |
74 | 78 | request = MockRequest() |
75 | 79 | m = ChildAdmin(Child, admin.site) |
76 | 80 | |
… |
… |
class ChangeListTests(TransactionTestCase):
|
136 | 140 | self.assertIsInstance(cl.paginator, CustomPaginator) |
137 | 141 | |
138 | 142 | |
| 143 | def test_distinct(self): |
| 144 | """ |
| 145 | Regression test for #13902: When using a ManyToMany in list_filter, |
| 146 | results may apper more than once |
| 147 | """ |
| 148 | new_parent = self.new_parent |
| 149 | new_child = self.new_child |
| 150 | relation1 = Through.objects.create(parent=new_parent, child=new_child) |
| 151 | relation2 = Through.objects.create(parent=new_parent, child=new_child) |
| 152 | |
| 153 | m = ChildAdmin(Child, admin.site) |
| 154 | cl = ChangeList(MockFilteredRequest(), Child, m.list_display, m.list_display_links, |
| 155 | m.list_filter, m.date_hierarchy, m.search_fields, |
| 156 | m.list_select_related, m.list_per_page, m.list_editable, m) |
| 157 | |
| 158 | cl.get_results(MockFilteredRequest()) |
| 159 | |
| 160 | # There's only one Child instance |
| 161 | self.assertEqual(cl.result_count, 1) |
| 162 | |
139 | 163 | class ChildAdmin(admin.ModelAdmin): |
140 | 164 | list_display = ['name', 'parent'] |
141 | 165 | def queryset(self, request): |
142 | 166 | return super(ChildAdmin, self).queryset(request).select_related("parent__name") |
| 167 | list_filter = ['multiple_m2m', ] |
143 | 168 | |
144 | 169 | |
145 | 170 | class MockRequest(object): |
146 | 171 | GET = {} |
147 | 172 | |
| 173 | class MockFilteredRequest(object): |
| 174 | GET = {'multiple_m2m__id__exact': 1, } |
148 | 175 | |
149 | 176 | class CustomPaginator(Paginator): |
150 | 177 | def __init__(self, queryset, page_size, orphans=0, allow_empty_first_page=True): |
151 | 178 | super(CustomPaginator, self).__init__(queryset, 5, orphans=2, |
152 | 179 | allow_empty_first_page=allow_empty_first_page) |
| 180 | |