Code

Ticket #13902: 13902_distinct_in_changelist.diff

File 13902_distinct_in_changelist.diff, 5.5 KB (added by rasca, 4 years ago)

fixes #13902

Line 
1diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
2index 8c09c10..2fa9c96 100644
3--- a/django/contrib/admin/views/main.py
4+++ b/django/contrib/admin/views/main.py
5@@ -164,12 +164,18 @@ class ChangeList(object):
6         return order_field, order_type
7 
8     def get_query_set(self):
9+        DISTINCT = False
10+
11         qs = self.root_query_set
12         lookup_params = self.params.copy() # a dictionary of the query string
13         for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR):
14             if i in lookup_params:
15                 del lookup_params[i]
16         for key, value in lookup_params.items():
17+            if not DISTINCT and key.count('__') > 1:
18+                # Check if it's a relationship that might return more than one
19+                # instance
20+                DISTINCT = True
21             if not isinstance(key, str):
22                 # 'key' will be used as a keyword argument later, so Python
23                 # requires it to be a string.
24@@ -230,16 +236,19 @@ class ChangeList(object):
25             else:
26                 return "%s__icontains" % field_name
27 
28-        if self.search_fields and self.query:
29+        if not DISTINCT and self.search_fields and self.query:
30             for bit in self.query.split():
31                 or_queries = [models.Q(**{construct_search(str(field_name)): bit}) for field_name in self.search_fields]
32                 qs = qs.filter(reduce(operator.or_, or_queries))
33             for field_name in self.search_fields:
34                 if '__' in field_name:
35-                    qs = qs.distinct()
36+                    DISCTINCT = True
37                     break
38 
39-        return qs
40+        if DISTINCT:
41+            return qs.distinct()
42+        else:
43+            return qs
44 
45     def url_for_result(self, result):
46         return "%s/" % quote(getattr(result, self.pk_attname))
47diff --git a/tests/regressiontests/admin_changelist/models.py b/tests/regressiontests/admin_changelist/models.py
48index f030a78..c043c8a 100644
49--- a/tests/regressiontests/admin_changelist/models.py
50+++ b/tests/regressiontests/admin_changelist/models.py
51@@ -6,4 +6,10 @@ class Parent(models.Model):
52 
53 class Child(models.Model):
54     parent = models.ForeignKey(Parent, editable=False)
55-    name = models.CharField(max_length=30, blank=True)
56\ No newline at end of file
57+    name = models.CharField(max_length=30, blank=True)
58+    multiple_m2m = models.ManyToManyField(Parent, related_name='multiple_m2m',
59+                                          through='Through')
60+
61+class Through(models.Model):
62+    parent = models.ForeignKey(Parent)
63+    child = models.ForeignKey(Child)
64diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
65index b70d7c5..569bfe8 100644
66--- a/tests/regressiontests/admin_changelist/tests.py
67+++ b/tests/regressiontests/admin_changelist/tests.py
68@@ -2,9 +2,17 @@ import unittest
69 from django.contrib import admin
70 from django.contrib.admin.views.main import ChangeList
71 from django.template import Context, Template
72-from regressiontests.admin_changelist.models import Child, Parent
73+from regressiontests.admin_changelist.models import Child, Parent, Through
74 
75 class ChangeListTests(unittest.TestCase):
76+    def setUp(self):
77+        self.new_parent = Parent.objects.create(name='parent')
78+        self.new_child = Child.objects.create(name='name', parent=self.new_parent)
79+
80+    def tearDown(self):
81+        self.new_parent.delete()
82+        self.new_child.delete()
83+
84     def test_select_related_preserved(self):
85         """
86         Regression test for #10348: ChangeList.get_query_set() shouldn't
87@@ -22,8 +30,8 @@ class ChangeListTests(unittest.TestCase):
88         table and this checks that the items are nested within the table
89         element tags.
90         """
91-        new_parent = Parent.objects.create(name='parent')
92-        new_child = Child.objects.create(name='name', parent=new_parent)
93+        new_parent = self.new_parent
94+        new_child = self.new_child
95         request = MockRequest()
96         m = ChildAdmin(Child, admin.site)
97         cl = ChangeList(request, Child, m.list_display, m.list_display_links,
98@@ -57,10 +65,35 @@ class ChangeListTests(unittest.TestCase):
99         self.failIf(table_output.find('<td>%s</td>' % hidden_input_elem) == -1,
100             'Hidden input element is not enclosed in <td> element.')
101 
102+    def test_distinct(self):
103+        """
104+        Regression test for #13902: When using a ManyToMany in list_filter,
105+        results may apper more than once
106+        """
107+        new_parent = self.new_parent
108+        new_child = self.new_child
109+        relation1 = Through.objects.create(parent=new_parent, child=new_child)
110+        relation2 = Through.objects.create(parent=new_parent, child=new_child)
111+
112+        m = ChildAdmin(Child, admin.site)
113+        cl = ChangeList(MockFilteredRequest(), Child, m.list_display, m.list_display_links,
114+                m.list_filter, m.date_hierarchy, m.search_fields,
115+                m.list_select_related, m.list_per_page, m.list_editable, m)
116+
117+        cl.get_results(MockFilteredRequest())
118+
119+        # There's only one Child instance
120+        self.assertEqual(cl.result_count, 1)
121+       
122 class ChildAdmin(admin.ModelAdmin):
123     list_display = ['name', 'parent']
124     def queryset(self, request):
125         return super(ChildAdmin, self).queryset(request).select_related("parent__name")
126+    list_filter = ['multiple_m2m', ]
127 
128 class MockRequest(object):
129     GET = {}
130+
131+class MockFilteredRequest(object):
132+    GET = {'multiple_m2m__id__exact': 1, }
133+