Code

Ticket #19755: 19755.diff

File 19755.diff, 15.9 KB (added by suligap, 12 months ago)
Line 
1diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
2index ff66d3e..c4f7ca7 100644
3--- a/django/contrib/admin/filters.py
4+++ b/django/contrib/admin/filters.py
5@@ -54,6 +54,9 @@ class ListFilter(object):
6         """
7         raise NotImplementedError
8 
9+    def limit_lookup_choices(self, queryset):
10+        pass
11+
12 
13 class SimpleListFilter(ListFilter):
14     # The parameter that should be used in the query string for that filter.
15@@ -118,6 +121,7 @@ class FieldListFilter(ListFilter):
16         self.field = field
17         self.field_path = field_path
18         self.title = getattr(field, 'verbose_name', field_path)
19+        self.limited_qs = None
20         super(FieldListFilter, self).__init__(
21             request, params, model, model_admin)
22         for p in self.expected_parameters():
23@@ -134,6 +138,9 @@ class FieldListFilter(ListFilter):
24         except ValidationError as e:
25             raise IncorrectLookupParameters(e)
26 
27+    def limit_lookup_choices(self, queryset):
28+        self.limited_qs = queryset
29+
30     @classmethod
31     def register(cls, test, list_filter_class, take_priority=False):
32         if take_priority:
33@@ -157,23 +164,23 @@ class FieldListFilter(ListFilter):
34 
35 class RelatedFieldListFilter(FieldListFilter):
36     def __init__(self, field, request, params, model, model_admin, field_path):
37-        other_model = get_model_from_relation(field)
38+        self.other_model = get_model_from_relation(field)
39         if hasattr(field, 'rel'):
40             rel_name = field.rel.get_related_field().name
41         else:
42-            rel_name = other_model._meta.pk.name
43+            rel_name = self.other_model._meta.pk.name
44         self.lookup_kwarg = '%s__%s__exact' % (field_path, rel_name)
45         self.lookup_kwarg_isnull = '%s__isnull' % field_path
46         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
47         self.lookup_val_isnull = request.GET.get(
48                                       self.lookup_kwarg_isnull, None)
49-        self.lookup_choices = field.get_choices(include_blank=False)
50+        self._lookup_choices = None
51         super(RelatedFieldListFilter, self).__init__(
52             field, request, params, model, model_admin, field_path)
53         if hasattr(field, 'verbose_name'):
54             self.lookup_title = field.verbose_name
55         else:
56-            self.lookup_title = other_model._meta.verbose_name
57+            self.lookup_title = self.other_model._meta.verbose_name
58         self.title = self.lookup_title
59 
60     def has_output(self):
61@@ -183,11 +190,24 @@ class RelatedFieldListFilter(FieldListFilter):
62             extra = 1
63         else:
64             extra = 0
65-        return len(self.lookup_choices) + extra > 1
66+        return len(self.lookup_choices) + extra > 0
67 
68     def expected_parameters(self):
69         return [self.lookup_kwarg, self.lookup_kwarg_isnull]
70 
71+    def _get_lookup_choices(self):
72+        if self._lookup_choices is None:
73+            if self.limited_qs is None:
74+                self._lookup_choices = self.field.get_choices(include_blank=False)
75+            else:
76+                pks = (self.limited_qs
77+                       .distinct()
78+                       .values_list(self.field_path, flat=True))
79+                self._lookup_choices = [(x._get_pk_val(), smart_text(x)) for x in
80+                                        self.other_model.objects.filter(pk__in=pks)]
81+        return self._lookup_choices
82+    lookup_choices = property(_get_lookup_choices)
83+
84     def choices(self, cl):
85         from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
86         yield {
87@@ -233,10 +253,22 @@ class BooleanFieldListFilter(FieldListFilter):
88         return [self.lookup_kwarg, self.lookup_kwarg2]
89 
90     def choices(self, cl):
91-        for lookup, title in (
92-                (None, _('All')),
93-                ('1', _('Yes')),
94-                ('0', _('No'))):
95+        lookup, title = None, _('All')
96+        yield {
97+            'selected': self.lookup_val == lookup and not self.lookup_val2,
98+            'query_string': cl.get_query_string({
99+                    self.lookup_kwarg: lookup,
100+                }, [self.lookup_kwarg2]),
101+            'display': title,
102+        }
103+        choices = self.lookup_choices()
104+        for choice in choices:
105+            if choice is None:
106+                continue
107+            elif choice:
108+                lookup, title = '1', _('Yes')
109+            else:
110+                lookup, title = '0', _('No')
111             yield {
112                 'selected': self.lookup_val == lookup and not self.lookup_val2,
113                 'query_string': cl.get_query_string({
114@@ -244,7 +276,7 @@ class BooleanFieldListFilter(FieldListFilter):
115                     }, [self.lookup_kwarg2]),
116                 'display': title,
117             }
118-        if isinstance(self.field, models.NullBooleanField):
119+        if None in choices:
120             yield {
121                 'selected': self.lookup_val2 == 'True',
122                 'query_string': cl.get_query_string({
123@@ -253,6 +285,19 @@ class BooleanFieldListFilter(FieldListFilter):
124                 'display': _('Unknown'),
125             }
126 
127+    def lookup_choices(self):
128+        if self.limited_qs is None:
129+            if isinstance(self.field, models.NullBooleanField):
130+                choices = (True, False, None)
131+            else:
132+                choices = (True, False)
133+        else:
134+            choices = (self.limited_qs
135+                       .distinct()
136+                       .values_list(self.field_path, flat=True))
137+            choices = tuple(ch for ch in (True, False, None) if ch in choices)
138+        return choices
139+
140 FieldListFilter.register(lambda f: isinstance(f,
141     (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
142 
143@@ -261,9 +306,13 @@ class ChoicesFieldListFilter(FieldListFilter):
144     def __init__(self, field, request, params, model, model_admin, field_path):
145         self.lookup_kwarg = '%s__exact' % field_path
146         self.lookup_val = request.GET.get(self.lookup_kwarg)
147+        self._lookup_choices = None
148         super(ChoicesFieldListFilter, self).__init__(
149             field, request, params, model, model_admin, field_path)
150 
151+    def has_output(self):
152+        return len(self.lookup_choices) > 0
153+
154     def expected_parameters(self):
155         return [self.lookup_kwarg]
156 
157@@ -273,7 +322,7 @@ class ChoicesFieldListFilter(FieldListFilter):
158             'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
159             'display': _('All')
160         }
161-        for lookup, title in self.field.flatchoices:
162+        for lookup, title in self.lookup_choices:
163             yield {
164                 'selected': smart_text(lookup) == self.lookup_val,
165                 'query_string': cl.get_query_string({
166@@ -281,6 +330,25 @@ class ChoicesFieldListFilter(FieldListFilter):
167                 'display': title,
168             }
169 
170+    def _get_lookup_choices(self):
171+        if self._lookup_choices is None:
172+            if self.limited_qs is None:
173+                self._lookup_choices = self.field.flatchoices
174+            else:
175+                choices_dict = dict(self.field.flatchoices)
176+                choices = (self.limited_qs
177+                           .distinct()
178+                           .order_by(self.field_path)
179+                           .values_list(self.field_path, flat=True))
180+                self._lookup_choices = []
181+                for choice in choices:
182+                    if choice is not None:
183+                        title = force_text(choices_dict.get(choice, choice),
184+                                           strings_only=True)
185+                        self._lookup_choices.append((choice, title))
186+        return self._lookup_choices
187+    lookup_choices = property(_get_lookup_choices)
188+
189 FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
190 
191 
192@@ -302,30 +370,18 @@ class DateFieldListFilter(FieldListFilter):
193             today = now.date()
194         tomorrow = today + datetime.timedelta(days=1)
195 
196+        self.today = today
197+        self.tomorrow = tomorrow
198+
199         self.lookup_kwarg_since = '%s__gte' % field_path
200         self.lookup_kwarg_until = '%s__lt' % field_path
201-        self.links = (
202-            (_('Any date'), {}),
203-            (_('Today'), {
204-                self.lookup_kwarg_since: str(today),
205-                self.lookup_kwarg_until: str(tomorrow),
206-            }),
207-            (_('Past 7 days'), {
208-                self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)),
209-                self.lookup_kwarg_until: str(tomorrow),
210-            }),
211-            (_('This month'), {
212-                self.lookup_kwarg_since: str(today.replace(day=1)),
213-                self.lookup_kwarg_until: str(tomorrow),
214-            }),
215-            (_('This year'), {
216-                self.lookup_kwarg_since: str(today.replace(month=1, day=1)),
217-                self.lookup_kwarg_until: str(tomorrow),
218-            }),
219-        )
220+        self._links = None
221         super(DateFieldListFilter, self).__init__(
222             field, request, params, model, model_admin, field_path)
223 
224+    def has_output(self):
225+        return len(self.links) > 1
226+
227     def expected_parameters(self):
228         return [self.lookup_kwarg_since, self.lookup_kwarg_until]
229 
230@@ -338,6 +394,36 @@ class DateFieldListFilter(FieldListFilter):
231                 'display': title,
232             }
233 
234+    def _get_links(self):
235+        if self._links is None:
236+            self._links = []
237+            qs = self.limited_qs
238+            queries = (
239+                (_('Today'), (
240+                    (self.lookup_kwarg_since, self.today),
241+                    (self.lookup_kwarg_until, self.tomorrow),
242+                )),
243+                (_('Past 7 days'), (
244+                    (self.lookup_kwarg_since, self.today - datetime.timedelta(days=7)),
245+                    (self.lookup_kwarg_until, self.tomorrow),
246+                )),
247+                (_('This month'), (
248+                    (self.lookup_kwarg_since, self.today.replace(day=1)),
249+                    (self.lookup_kwarg_until, self.tomorrow),
250+                )),
251+                (_('This year'), (
252+                    (self.lookup_kwarg_since, self.today.replace(day=1, month=1)),
253+                    (self.lookup_kwarg_until, self.tomorrow),
254+                )),
255+            )
256+            self._links.append((_('Any date'), {}))
257+            for title, query in queries:
258+                if qs is None or qs.filter(**dict(query)).exists():
259+                    self._links.append((title, dict((k, str(v)) for k, v in query)))
260+        return self._links
261+    links = property(_get_links)
262+
263+
264 FieldListFilter.register(
265     lambda f: isinstance(f, models.DateField), DateFieldListFilter)
266 
267@@ -352,21 +438,22 @@ class AllValuesFieldListFilter(FieldListFilter):
268         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
269         self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
270                                                  None)
271-        parent_model, reverse_path = reverse_field_path(model, field_path)
272+        parent_model, self.reverse_path = reverse_field_path(model, field_path)
273         queryset = parent_model._default_manager.all()
274         # optional feature: limit choices base on existing relationships
275         # queryset = queryset.complex_filter(
276         #    {'%s__isnull' % reverse_path: False})
277         limit_choices_to = get_limit_choices_to_from_path(model, field_path)
278         queryset = queryset.filter(limit_choices_to)
279+        self.field_qs = queryset
280 
281-        self.lookup_choices = (queryset
282-                               .distinct()
283-                               .order_by(field.name)
284-                               .values_list(field.name, flat=True))
285+        self._lookup_choices = None
286         super(AllValuesFieldListFilter, self).__init__(
287             field, request, params, model, model_admin, field_path)
288 
289+    def has_output(self):
290+        return len(self.lookup_choices) > 0
291+
292     def expected_parameters(self):
293         return [self.lookup_kwarg, self.lookup_kwarg_isnull]
294 
295@@ -401,4 +488,19 @@ class AllValuesFieldListFilter(FieldListFilter):
296                 'display': EMPTY_CHANGELIST_VALUE,
297             }
298 
299+    def _get_lookup_choices(self):
300+        if self._lookup_choices is None:
301+            if self.limited_qs is None:
302+                qs = self.field_qs
303+                field_name = self.field.name
304+            else:
305+                qs = self.limited_qs
306+                field_name = self.field_path
307+            self._lookup_choices = (qs
308+                                    .distinct()
309+                                    .order_by(field_name)
310+                                    .values_list(field_name, flat=True))
311+        return self._lookup_choices
312+    lookup_choices = property(_get_lookup_choices)
313+
314 FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
315diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
316index b35f100..ee8759f 100644
317--- a/django/contrib/admin/options.py
318+++ b/django/contrib/admin/options.py
319@@ -345,6 +345,7 @@ class ModelAdmin(BaseModelAdmin):
320     list_display = ('__str__',)
321     list_display_links = ()
322     list_filter = ()
323+    list_filter_incremental = False
324     list_select_related = False
325     list_per_page = 100
326     list_max_show_all = 200
327diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
328index a02bb7a..1efbcd7 100644
329--- a/django/contrib/admin/validation.py
330+++ b/django/contrib/admin/validation.py
331@@ -163,10 +163,11 @@ def validate(cls, model):
332     if hasattr(cls, "readonly_fields"):
333         check_readonly_fields(cls, model, opts)
334 
335+    # list_filter_incremental = False
336     # list_select_related = False
337     # save_as = False
338     # save_on_top = False
339-    for attr in ('list_select_related', 'save_as', 'save_on_top'):
340+    for attr in ('list_filter_incremental', 'list_select_related', 'save_as', 'save_on_top'):
341         if not isinstance(getattr(cls, attr), bool):
342             raise ImproperlyConfigured("'%s.%s' should be a boolean."
343                     % (cls.__name__, attr))
344diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
345index 050d477..9b78561 100644
346--- a/django/contrib/admin/views/main.py
347+++ b/django/contrib/admin/views/main.py
348@@ -158,7 +158,8 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
349                     use_distinct = (use_distinct or
350                                     lookup_needs_distinct(self.lookup_opts,
351                                                           field_path))
352-                if spec and spec.has_output():
353+                incremental = self.model_admin.list_filter_incremental
354+                if spec and (incremental or spec.has_output()):
355                     filter_specs.append(spec)
356 
357         # At this point, all the parameters used by the various ListFilters
358@@ -330,16 +331,24 @@ class ChangeList(six.with_metaclass(RenameChangeListMethods)):
359 
360     def get_queryset(self, request):
361         # First, we collect all the declared list filters.
362-        (self.filter_specs, self.has_filters, remaining_lookup_params,
363+        (filter_specs, self.has_filters, remaining_lookup_params,
364          use_distinct) = self.get_filters(request)
365 
366         # Then, we let every list filter modify the queryset to its liking.
367         qs = self.root_queryset
368-        for filter_spec in self.filter_specs:
369+        for filter_spec in filter_specs:
370             new_qs = filter_spec.queryset(request, qs)
371             if new_qs is not None:
372                 qs = new_qs
373 
374+        # incremental list filter, limit lookup choices
375+        if self.model_admin.list_filter_incremental:
376+            for filter_spec in filter_specs:
377+                filter_spec.limit_lookup_choices(qs)
378+
379+        # only filters with output are saved
380+        self.filter_specs = [fs for fs in filter_specs if fs.has_output()]
381+
382         try:
383             # Finally, we apply the remaining lookup parameters from the query
384             # string (i.e. those that haven't already been processed by the