Code

Ticket #8528: 8528_filterspec_null.diff

File 8528_filterspec_null.diff, 11.9 KB (added by julien, 3 years ago)
Line 
1diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
2index eab5407..a18a6e5 100644
3--- a/django/contrib/admin/filterspecs.py
4+++ b/django/contrib/admin/filterspecs.py
5@@ -76,7 +76,9 @@ class RelatedFilterSpec(FilterSpec):
6             self.lookup_title = f.verbose_name # use field name
7         rel_name = other_model._meta.pk.name
8         self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
9+        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
10         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
11+        self.lookup_val_isnull = bool(request.GET.get(self.lookup_kwarg_isnull, None))
12         self.lookup_choices = f.get_choices(include_blank=False)
13 
14     def has_output(self):
15@@ -86,13 +88,18 @@ class RelatedFilterSpec(FilterSpec):
16         return self.lookup_title
17 
18     def choices(self, cl):
19-        yield {'selected': self.lookup_val is None,
20-               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
21+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
22+        yield {'selected': self.lookup_val is None and not self.lookup_val_isnull,
23+               'query_string': cl.get_query_string({}, [self.lookup_kwarg, self.lookup_kwarg_isnull]),
24                'display': _('All')}
25         for pk_val, val in self.lookup_choices:
26             yield {'selected': self.lookup_val == smart_unicode(pk_val),
27-                   'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}),
28+                   'query_string': cl.get_query_string({self.lookup_kwarg: pk_val}, [self.lookup_kwarg_isnull]),
29                    'display': val}
30+        if hasattr(self.field, 'rel') and self.field.null:
31+            yield {'selected': self.lookup_val_isnull,
32+                   'query_string': cl.get_query_string({self.lookup_kwarg_isnull: 'True'}, [self.lookup_kwarg]),
33+                   'display': EMPTY_CHANGELIST_VALUE}
34 
35 FilterSpec.register(lambda f: (
36         hasattr(f, 'rel') and bool(f.rel) or
37@@ -192,8 +199,11 @@ class AllValuesFilterSpec(FilterSpec):
38         super(AllValuesFilterSpec, self).__init__(f, request, params, model,
39                                                   model_admin,
40                                                   field_path=field_path)
41-        self.lookup_val = request.GET.get(self.field_path, None)
42-        parent_model, reverse_path = reverse_field_path(model, field_path)
43+        self.lookup_kwarg = self.field_path
44+        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
45+        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
46+        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None)
47+        parent_model, reverse_path = reverse_field_path(model, self.field_path)
48         queryset = parent_model._default_manager.all()
49         # optional feature: limit choices base on existing relationships
50         # queryset = queryset.complex_filter(
51@@ -202,18 +212,30 @@ class AllValuesFilterSpec(FilterSpec):
52         queryset = queryset.filter(limit_choices_to)
53 
54         self.lookup_choices = \
55-            queryset.distinct().order_by(f.name).values(f.name)
56+            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
57 
58     def title(self):
59         return self.field.verbose_name
60 
61     def choices(self, cl):
62-        yield {'selected': self.lookup_val is None,
63-               'query_string': cl.get_query_string({}, [self.field_path]),
64+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
65+        yield {'selected': self.lookup_val is None and self.lookup_val_isnull is None,
66+               'query_string': cl.get_query_string({}, [self.lookup_kwarg, self.lookup_kwarg_isnull]),
67                'display': _('All')}
68+        include_none = False
69+       
70         for val in self.lookup_choices:
71-            val = smart_unicode(val[self.field.name])
72+            if val is None:
73+                include_none = True
74+                continue
75+            val = smart_unicode(val)
76+           
77             yield {'selected': self.lookup_val == val,
78-                   'query_string': cl.get_query_string({self.field_path: val}),
79+                   'query_string': cl.get_query_string({self.lookup_kwarg: val}, [self.lookup_kwarg_isnull]),
80                    'display': val}
81+        if include_none:
82+            yield {'selected': self.lookup_val_isnull is not None,
83+                    'query_string': cl.get_query_string({self.lookup_kwarg_isnull: 'True'}, [self.lookup_kwarg]),
84+                    'display': EMPTY_CHANGELIST_VALUE}
85+
86 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
87diff --git a/tests/regressiontests/admin_filterspecs/__init__.py b/tests/regressiontests/admin_filterspecs/__init__.py
88new file mode 100644
89index 0000000..e69de29
90diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
91new file mode 100644
92index 0000000..e62a070
93--- /dev/null
94+++ b/tests/regressiontests/admin_filterspecs/models.py
95@@ -0,0 +1,8 @@
96+from django.db import models
97+from django.contrib.auth.models import User
98+
99+class Book(models.Model):
100+    title = models.CharField(max_length=25)
101+    year = models.PositiveIntegerField(null=True, blank=True)
102+    author = models.ForeignKey(User, related_name='books_authored', blank=True, null=True)
103+    contributors = models.ManyToManyField(User, related_name='books_contributed', blank=True, null=True)
104diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
105new file mode 100644
106index 0000000..e8be8cf
107--- /dev/null
108+++ b/tests/regressiontests/admin_filterspecs/tests.py
109@@ -0,0 +1,119 @@
110+from django.test import TestCase
111+from django.test.client import RequestFactory
112+from django.contrib.auth.models import User
113+
114+from django.contrib import admin
115+from django.contrib.admin.views.main import ChangeList
116+
117+from models import Book
118+
119+class FilterSpecsTests(TestCase):
120+   
121+    def setUp(self):
122+        # Users
123+        alfred = User.objects.create_user('alfred', 'alfred@example.com')
124+        bob = User.objects.create_user('bob', 'alfred@example.com')
125+        lisa = User.objects.create_user('lisa', 'lisa@example.com')
126+       
127+        #Books
128+        bio_book = Book.objects.create(title='Django: a biography', year=1999, author=alfred)
129+        django_book = Book.objects.create(title='The Django Book', year=None, author=bob)
130+        gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
131+        gipsy_book.contributors = [bob, lisa]
132+        gipsy_book.save()
133+       
134+        self.request_factory = RequestFactory()
135+       
136+    def test_AllValuesFilterSpec(self):
137+        modeladmin = BookAdmin(Book, admin.site)
138+       
139+        request = self.request_factory.get('/', {'year__isnull': 'True'})
140+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
141+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
142+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
143+           
144+        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
145+        queryset = changelist.get_query_set()
146+
147+        # Make sure the last choice is None and is selected
148+        filterspec = changelist.get_filters(request)[0][0]
149+        self.assertEquals(filterspec.title(), u'year')
150+        choices = list(filterspec.choices(changelist))
151+        self.assertEquals(choices[3]['selected'], True)
152+        self.assertEquals(choices[3]['query_string'], '?year__isnull=True')
153+       
154+        request = self.request_factory.get('/', {'year': '2002'})
155+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
156+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
157+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
158+           
159+        # Make sure the correct choice is selected   
160+        filterspec = changelist.get_filters(request)[0][0]
161+        self.assertEquals(filterspec.title(), u'year')
162+        choices = list(filterspec.choices(changelist))
163+        self.assertEquals(choices[2]['selected'], True)
164+        self.assertEquals(choices[2]['query_string'], '?year=2002')
165+       
166+    def test_RelatedFilterSpec_ForeignKey(self):
167+        modeladmin = BookAdmin(Book, admin.site)
168+       
169+        request = self.request_factory.get('/', {'author__isnull': 'True'})
170+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
171+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
172+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
173+       
174+        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
175+        queryset = changelist.get_query_set()
176+
177+        # Make sure the last choice is None and is selected
178+        filterspec = changelist.get_filters(request)[0][1]
179+        self.assertEquals(filterspec.title(), u'author')
180+        choices = list(filterspec.choices(changelist))
181+        self.assertEquals(choices[4]['selected'], True)
182+        self.assertEquals(choices[4]['query_string'], '?author__isnull=True')
183+       
184+        request = self.request_factory.get('/', {'author__id__exact': '1'})
185+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
186+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
187+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
188+           
189+        # Make sure the correct choice is selected
190+        filterspec = changelist.get_filters(request)[0][1]
191+        self.assertEquals(filterspec.title(), u'author')
192+        choices = list(filterspec.choices(changelist))
193+        self.assertEquals(choices[1]['selected'], True)
194+        self.assertEquals(choices[1]['query_string'], '?author__id__exact=1')
195+       
196+    def test_RelatedFilterSpec_ManyToMany(self):
197+        modeladmin = BookAdmin(Book, admin.site)
198+       
199+        request = self.request_factory.get('/', {'contributors__isnull': 'True'})
200+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
201+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
202+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
203+       
204+        # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
205+        queryset = changelist.get_query_set()
206+
207+        # Make sure the last choice is None and is selected
208+        filterspec = changelist.get_filters(request)[0][2]
209+        self.assertEquals(filterspec.title(), u'user')
210+        choices = list(filterspec.choices(changelist))
211+        self.assertEquals(choices[4]['selected'], True)
212+        self.assertEquals(choices[4]['query_string'], '?contributors__isnull=True')
213+       
214+        request = self.request_factory.get('/', {'contributors__id__exact': '2'})
215+        changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
216+            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
217+            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
218+           
219+        # Make sure the correct choice is selected
220+        filterspec = changelist.get_filters(request)[0][2]
221+        self.assertEquals(filterspec.title(), u'user')
222+        choices = list(filterspec.choices(changelist))
223+        self.assertEquals(choices[2]['selected'], True)
224+        self.assertEquals(choices[2]['query_string'], '?contributors__id__exact=2')
225+
226+class BookAdmin(admin.ModelAdmin):
227+    list_filter = ('year', 'author', 'contributors')
228+    order_by = '-id'