Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #5833 -- Modified the admin list filters to be easier to custom…

…ize. Many thanks to Honza Král, Tom X. Tobin, gerdemb, eandre, sciyoshi, bendavis78 and Julien Phalip for working on this.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16144 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 18d2f4a81611fa4051ccdcfb9dd443f3a247102e 1 parent a85cd16
Jannis Leidel authored May 03, 2011
3  django/contrib/admin/__init__.py
@@ -4,6 +4,9 @@
4 4
 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
5 5
 from django.contrib.admin.options import StackedInline, TabularInline
6 6
 from django.contrib.admin.sites import AdminSite, site
  7
+from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
  8
+    FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
  9
+    ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter)
7 10
 
8 11
 
9 12
 def autodiscover():
398  django/contrib/admin/filters.py
... ...
@@ -0,0 +1,398 @@
  1
+"""
  2
+This encapsulates the logic for displaying filters in the Django admin.
  3
+Filters are specified in models with the "list_filter" option.
  4
+
  5
+Each filter subclass knows how to display a filter for a field that passes a
  6
+certain test -- e.g. being a DateField or ForeignKey.
  7
+"""
  8
+import datetime
  9
+
  10
+from django.db import models
  11
+from django.core.exceptions import ImproperlyConfigured
  12
+from django.utils.encoding import smart_unicode
  13
+from django.utils.translation import ugettext_lazy as _
  14
+
  15
+from django.contrib.admin.util import (get_model_from_relation,
  16
+    reverse_field_path, get_limit_choices_to_from_path)
  17
+
  18
+class ListFilter(object):
  19
+    title = None  # Human-readable title to appear in the right sidebar.
  20
+
  21
+    def __init__(self, request, params, model, model_admin):
  22
+        self.params = params
  23
+        if self.title is None:
  24
+            raise ImproperlyConfigured(
  25
+                "The list filter '%s' does not specify "
  26
+                "a 'title'." % self.__class__.__name__)
  27
+
  28
+    def has_output(self):
  29
+        """
  30
+        Returns True if some choices would be output for the filter.
  31
+        """
  32
+        raise NotImplementedError
  33
+
  34
+    def choices(self, cl):
  35
+        """
  36
+        Returns choices ready to be output in the template.
  37
+        """
  38
+        raise NotImplementedError
  39
+
  40
+    def queryset(self, request, queryset):
  41
+        """
  42
+        Returns the filtered queryset.
  43
+        """
  44
+        raise NotImplementedError
  45
+
  46
+    def used_params(self):
  47
+        """
  48
+        Return a list of parameters to consume from the change list
  49
+        querystring.
  50
+        """
  51
+        raise NotImplementedError
  52
+
  53
+
  54
+
  55
+class SimpleListFilter(ListFilter):
  56
+    # The parameter that should be used in the query string for that filter.
  57
+    parameter_name = None
  58
+
  59
+    def __init__(self, request, params, model, model_admin):
  60
+        super(SimpleListFilter, self).__init__(
  61
+            request, params, model, model_admin)
  62
+        if self.parameter_name is None:
  63
+            raise ImproperlyConfigured(
  64
+                "The list filter '%s' does not specify "
  65
+                "a 'parameter_name'." % self.__class__.__name__)
  66
+        self.lookup_choices = self.lookups(request)
  67
+
  68
+    def has_output(self):
  69
+        return len(self.lookup_choices) > 0
  70
+
  71
+    def value(self):
  72
+        """
  73
+        Returns the value given in the query string for this filter,
  74
+        if any. Returns None otherwise.
  75
+        """
  76
+        return self.params.get(self.parameter_name, None)
  77
+
  78
+    def lookups(self, request):
  79
+        """
  80
+        Must be overriden to return a list of tuples (value, verbose value)
  81
+        """
  82
+        raise NotImplementedError
  83
+
  84
+    def used_params(self):
  85
+        return [self.parameter_name]
  86
+
  87
+    def choices(self, cl):
  88
+        yield {
  89
+            'selected': self.value() is None,
  90
+            'query_string': cl.get_query_string({}, [self.parameter_name]),
  91
+            'display': _('All'),
  92
+        }
  93
+        for lookup, title in self.lookup_choices:
  94
+            yield {
  95
+                'selected': self.value() == lookup,
  96
+                'query_string': cl.get_query_string({
  97
+                    self.parameter_name: lookup,
  98
+                }, []),
  99
+                'display': title,
  100
+            }
  101
+
  102
+
  103
+class FieldListFilter(ListFilter):
  104
+    _field_list_filters = []
  105
+    _take_priority_index = 0
  106
+
  107
+    def __init__(self, field, request, params, model, model_admin, field_path):
  108
+        self.field = field
  109
+        self.field_path = field_path
  110
+        self.title = field_path
  111
+        super(FieldListFilter, self).__init__(request, params, model, model_admin)
  112
+
  113
+    def has_output(self):
  114
+        return True
  115
+
  116
+    def queryset(self, request, queryset):
  117
+        for p in self.used_params():
  118
+            if p in self.params:
  119
+                return queryset.filter(**{p: self.params[p]})
  120
+
  121
+    @classmethod
  122
+    def register(cls, test, list_filter_class, take_priority=False):
  123
+        if take_priority:
  124
+            # This is to allow overriding the default filters for certain types
  125
+            # of fields with some custom filters. The first found in the list
  126
+            # is used in priority.
  127
+            cls._field_list_filters.insert(
  128
+                cls._take_priority_index, (test, list_filter_class))
  129
+            cls._take_priority_index += 1
  130
+        else:
  131
+            cls._field_list_filters.append((test, list_filter_class))
  132
+
  133
+    @classmethod
  134
+    def create(cls, field, request, params, model, model_admin, field_path):
  135
+        for test, list_filter_class in cls._field_list_filters:
  136
+            if not test(field):
  137
+                continue
  138
+            return list_filter_class(field, request, params,
  139
+                model, model_admin, field_path=field_path)
  140
+
  141
+
  142
+class RelatedFieldListFilter(FieldListFilter):
  143
+    def __init__(self, field, request, params, model, model_admin, field_path):
  144
+        super(RelatedFieldListFilter, self).__init__(
  145
+            field, request, params, model, model_admin, field_path)
  146
+
  147
+        other_model = get_model_from_relation(field)
  148
+        if isinstance(field, (models.ManyToManyField,
  149
+                          models.related.RelatedObject)):
  150
+            # no direct field on this model, get name from other model
  151
+            self.lookup_title = other_model._meta.verbose_name
  152
+        else:
  153
+            self.lookup_title = field.verbose_name # use field name
  154
+        rel_name = other_model._meta.pk.name
  155
+        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
  156
+        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
  157
+        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
  158
+        self.lookup_val_isnull = request.GET.get(
  159
+                                      self.lookup_kwarg_isnull, None)
  160
+        self.lookup_choices = field.get_choices(include_blank=False)
  161
+        self.title = self.lookup_title
  162
+
  163
+    def has_output(self):
  164
+        if (isinstance(self.field, models.related.RelatedObject)
  165
+                and self.field.field.null or hasattr(self.field, 'rel')
  166
+                    and self.field.null):
  167
+            extra = 1
  168
+        else:
  169
+            extra = 0
  170
+        return len(self.lookup_choices) + extra > 1
  171
+
  172
+    def used_params(self):
  173
+        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
  174
+
  175
+    def choices(self, cl):
  176
+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  177
+        yield {
  178
+            'selected': self.lookup_val is None and not self.lookup_val_isnull,
  179
+            'query_string': cl.get_query_string({},
  180
+                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
  181
+            'display': _('All'),
  182
+        }
  183
+        for pk_val, val in self.lookup_choices:
  184
+            yield {
  185
+                'selected': self.lookup_val == smart_unicode(pk_val),
  186
+                'query_string': cl.get_query_string({
  187
+                    self.lookup_kwarg: pk_val,
  188
+                }, [self.lookup_kwarg_isnull]),
  189
+                'display': val,
  190
+            }
  191
+        if (isinstance(self.field, models.related.RelatedObject)
  192
+                and self.field.field.null or hasattr(self.field, 'rel')
  193
+                    and self.field.null):
  194
+            yield {
  195
+                'selected': bool(self.lookup_val_isnull),
  196
+                'query_string': cl.get_query_string({
  197
+                    self.lookup_kwarg_isnull: 'True',
  198
+                }, [self.lookup_kwarg]),
  199
+                'display': EMPTY_CHANGELIST_VALUE,
  200
+            }
  201
+
  202
+FieldListFilter.register(lambda f: (
  203
+        hasattr(f, 'rel') and bool(f.rel) or
  204
+        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
  205
+
  206
+
  207
+class BooleanFieldListFilter(FieldListFilter):
  208
+    def __init__(self, field, request, params, model, model_admin, field_path):
  209
+        super(BooleanFieldListFilter, self).__init__(field,
  210
+            request, params, model, model_admin, field_path)
  211
+        self.lookup_kwarg = '%s__exact' % self.field_path
  212
+        self.lookup_kwarg2 = '%s__isnull' % self.field_path
  213
+        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
  214
+        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
  215
+
  216
+    def used_params(self):
  217
+        return [self.lookup_kwarg, self.lookup_kwarg2]
  218
+
  219
+    def choices(self, cl):
  220
+        for lookup, title in (
  221
+                (None, _('All')),
  222
+                ('1', _('Yes')),
  223
+                ('0', _('No'))):
  224
+            yield {
  225
+                'selected': self.lookup_val == lookup and not self.lookup_val2,
  226
+                'query_string': cl.get_query_string({
  227
+                        self.lookup_kwarg: lookup,
  228
+                    }, [self.lookup_kwarg2]),
  229
+                'display': title,
  230
+            }
  231
+        if isinstance(self.field, models.NullBooleanField):
  232
+            yield {
  233
+                'selected': self.lookup_val2 == 'True',
  234
+                'query_string': cl.get_query_string({
  235
+                        self.lookup_kwarg2: 'True',
  236
+                    }, [self.lookup_kwarg]),
  237
+                'display': _('Unknown'),
  238
+            }
  239
+
  240
+FieldListFilter.register(lambda f: isinstance(f,
  241
+    (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
  242
+
  243
+
  244
+class ChoicesFieldListFilter(FieldListFilter):
  245
+    def __init__(self, field, request, params, model, model_admin, field_path):
  246
+        super(ChoicesFieldListFilter, self).__init__(
  247
+            field, request, params, model, model_admin, field_path)
  248
+        self.lookup_kwarg = '%s__exact' % self.field_path
  249
+        self.lookup_val = request.GET.get(self.lookup_kwarg)
  250
+
  251
+    def used_params(self):
  252
+        return [self.lookup_kwarg]
  253
+
  254
+    def choices(self, cl):
  255
+        yield {
  256
+            'selected': self.lookup_val is None,
  257
+            'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
  258
+            'display': _('All')
  259
+        }
  260
+        for lookup, title in self.field.flatchoices:
  261
+            yield {
  262
+                'selected': smart_unicode(lookup) == self.lookup_val,
  263
+                'query_string': cl.get_query_string({self.lookup_kwarg: lookup}),
  264
+                'display': title,
  265
+            }
  266
+
  267
+FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
  268
+
  269
+
  270
+class DateFieldListFilter(FieldListFilter):
  271
+    def __init__(self, field, request, params, model, model_admin, field_path):
  272
+        super(DateFieldListFilter, self).__init__(
  273
+            field, request, params, model, model_admin, field_path)
  274
+
  275
+        self.field_generic = '%s__' % self.field_path
  276
+        self.date_params = dict([(k, v) for k, v in params.items()
  277
+                                 if k.startswith(self.field_generic)])
  278
+
  279
+        today = datetime.date.today()
  280
+        one_week_ago = today - datetime.timedelta(days=7)
  281
+        today_str = (isinstance(self.field, models.DateTimeField)
  282
+                        and today.strftime('%Y-%m-%d 23:59:59')
  283
+                        or today.strftime('%Y-%m-%d'))
  284
+
  285
+        self.lookup_kwarg_year = '%s__year' % self.field_path
  286
+        self.lookup_kwarg_month = '%s__month' % self.field_path
  287
+        self.lookup_kwarg_day = '%s__day' % self.field_path
  288
+        self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
  289
+        self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
  290
+
  291
+        self.links = (
  292
+            (_('Any date'), {}),
  293
+            (_('Today'), {
  294
+                self.lookup_kwarg_year: str(today.year),
  295
+                self.lookup_kwarg_month: str(today.month),
  296
+                self.lookup_kwarg_day: str(today.day),
  297
+            }),
  298
+            (_('Past 7 days'), {
  299
+                self.lookup_kwarg_past_7_days_gte: one_week_ago.strftime('%Y-%m-%d'),
  300
+                self.lookup_kwarg_past_7_days_lte: today_str,
  301
+            }),
  302
+            (_('This month'), {
  303
+                self.lookup_kwarg_year: str(today.year),
  304
+                self.lookup_kwarg_month: str(today.month),
  305
+            }),
  306
+            (_('This year'), {
  307
+                self.lookup_kwarg_year: str(today.year),
  308
+            }),
  309
+        )
  310
+
  311
+    def used_params(self):
  312
+        return [
  313
+            self.lookup_kwarg_year, self.lookup_kwarg_month, self.lookup_kwarg_day,
  314
+            self.lookup_kwarg_past_7_days_gte, self.lookup_kwarg_past_7_days_lte
  315
+        ]
  316
+
  317
+    def queryset(self, request, queryset):
  318
+        """
  319
+        Override the default behaviour since there can be multiple query
  320
+        string parameters used for the same date filter (e.g. year + month).
  321
+        """
  322
+        query_dict = {}
  323
+        for p in self.used_params():
  324
+            if p in self.params:
  325
+                query_dict[p] = self.params[p]
  326
+        if len(query_dict):
  327
+            return queryset.filter(**query_dict)
  328
+
  329
+    def choices(self, cl):
  330
+        for title, param_dict in self.links:
  331
+            yield {
  332
+                'selected': self.date_params == param_dict,
  333
+                'query_string': cl.get_query_string(
  334
+                    param_dict, [self.field_generic]),
  335
+                'display': title,
  336
+            }
  337
+
  338
+FieldListFilter.register(
  339
+    lambda f: isinstance(f, models.DateField), DateFieldListFilter)
  340
+
  341
+
  342
+# This should be registered last, because it's a last resort. For example,
  343
+# if a field is eligible to use the BooleanFieldListFilter, that'd be much
  344
+# more appropriate, and the AllValuesFieldListFilter won't get used for it.
  345
+class AllValuesFieldListFilter(FieldListFilter):
  346
+    def __init__(self, field, request, params, model, model_admin, field_path):
  347
+        super(AllValuesFieldListFilter, self).__init__(
  348
+            field, request, params, model, model_admin, field_path)
  349
+        self.lookup_kwarg = self.field_path
  350
+        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
  351
+        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
  352
+        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None)
  353
+        parent_model, reverse_path = reverse_field_path(model, self.field_path)
  354
+        queryset = parent_model._default_manager.all()
  355
+        # optional feature: limit choices base on existing relationships
  356
+        # queryset = queryset.complex_filter(
  357
+        #    {'%s__isnull' % reverse_path: False})
  358
+        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
  359
+        queryset = queryset.filter(limit_choices_to)
  360
+
  361
+        self.lookup_choices = queryset.distinct(
  362
+            ).order_by(field.name).values_list(field.name, flat=True)
  363
+
  364
+    def used_params(self):
  365
+        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
  366
+
  367
+    def choices(self, cl):
  368
+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  369
+        yield {
  370
+            'selected': (self.lookup_val is None
  371
+                and self.lookup_val_isnull is None),
  372
+            'query_string': cl.get_query_string({},
  373
+                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
  374
+            'display': _('All'),
  375
+        }
  376
+        include_none = False
  377
+        for val in self.lookup_choices:
  378
+            if val is None:
  379
+                include_none = True
  380
+                continue
  381
+            val = smart_unicode(val)
  382
+            yield {
  383
+                'selected': self.lookup_val == val,
  384
+                'query_string': cl.get_query_string({
  385
+                    self.lookup_kwarg: val,
  386
+                }, [self.lookup_kwarg_isnull]),
  387
+                'display': val,
  388
+            }
  389
+        if include_none:
  390
+            yield {
  391
+                'selected': bool(self.lookup_val_isnull),
  392
+                'query_string': cl.get_query_string({
  393
+                    self.lookup_kwarg_isnull: 'True',
  394
+                }, [self.lookup_kwarg]),
  395
+                'display': EMPTY_CHANGELIST_VALUE,
  396
+            }
  397
+
  398
+FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
279  django/contrib/admin/filterspecs.py
... ...
@@ -1,279 +0,0 @@
1  
-"""
2  
-FilterSpec encapsulates the logic for displaying filters in the Django admin.
3  
-Filters are specified in models with the "list_filter" option.
4  
-
5  
-Each filter subclass knows how to display a filter for a field that passes a
6  
-certain test -- e.g. being a DateField or ForeignKey.
7  
-"""
8  
-
9  
-from django.db import models
10  
-from django.utils.encoding import smart_unicode, iri_to_uri
11  
-from django.utils.translation import ugettext as _
12  
-from django.utils.html import escape
13  
-from django.utils.safestring import mark_safe
14  
-from django.contrib.admin.util import get_model_from_relation, \
15  
-    reverse_field_path, get_limit_choices_to_from_path
16  
-import datetime
17  
-
18  
-class FilterSpec(object):
19  
-    filter_specs = []
20  
-    def __init__(self, f, request, params, model, model_admin,
21  
-                 field_path=None):
22  
-        self.field = f
23  
-        self.params = params
24  
-        self.field_path = field_path
25  
-        if field_path is None:
26  
-            if isinstance(f, models.related.RelatedObject):
27  
-                self.field_path = f.var_name
28  
-            else:
29  
-                self.field_path = f.name
30  
-
31  
-    def register(cls, test, factory):
32  
-        cls.filter_specs.append((test, factory))
33  
-    register = classmethod(register)
34  
-
35  
-    def create(cls, f, request, params, model, model_admin, field_path=None):
36  
-        for test, factory in cls.filter_specs:
37  
-            if test(f):
38  
-                return factory(f, request, params, model, model_admin,
39  
-                               field_path=field_path)
40  
-    create = classmethod(create)
41  
-
42  
-    def has_output(self):
43  
-        return True
44  
-
45  
-    def choices(self, cl):
46  
-        raise NotImplementedError()
47  
-
48  
-    def title(self):
49  
-        return self.field.verbose_name
50  
-
51  
-    def output(self, cl):
52  
-        t = []
53  
-        if self.has_output():
54  
-            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
55  
-
56  
-            for choice in self.choices(cl):
57  
-                t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
58  
-                    ((choice['selected'] and ' class="selected"' or ''),
59  
-                     iri_to_uri(choice['query_string']),
60  
-                     choice['display']))
61  
-            t.append('</ul>\n\n')
62  
-        return mark_safe("".join(t))
63  
-
64  
-class RelatedFilterSpec(FilterSpec):
65  
-    def __init__(self, f, request, params, model, model_admin,
66  
-                 field_path=None):
67  
-        super(RelatedFilterSpec, self).__init__(
68  
-            f, request, params, model, model_admin, field_path=field_path)
69  
-
70  
-        other_model = get_model_from_relation(f)
71  
-        if isinstance(f, (models.ManyToManyField,
72  
-                          models.related.RelatedObject)):
73  
-            # no direct field on this model, get name from other model
74  
-            self.lookup_title = other_model._meta.verbose_name
75  
-        else:
76  
-            self.lookup_title = f.verbose_name # use field name
77  
-        rel_name = other_model._meta.pk.name
78  
-        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
79  
-        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
80  
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
81  
-        self.lookup_val_isnull = request.GET.get(
82  
-                                      self.lookup_kwarg_isnull, None)
83  
-        self.lookup_choices = f.get_choices(include_blank=False)
84  
-
85  
-    def has_output(self):
86  
-        if isinstance(self.field, models.related.RelatedObject) \
87  
-           and self.field.field.null or hasattr(self.field, 'rel') \
88  
-           and self.field.null:
89  
-            extra = 1
90  
-        else:
91  
-            extra = 0
92  
-        return len(self.lookup_choices) + extra > 1
93  
-
94  
-    def title(self):
95  
-        return self.lookup_title
96  
-
97  
-    def choices(self, cl):
98  
-        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
99  
-        yield {'selected': self.lookup_val is None
100  
-                           and not self.lookup_val_isnull,
101  
-               'query_string': cl.get_query_string(
102  
-                               {},
103  
-                               [self.lookup_kwarg, self.lookup_kwarg_isnull]),
104  
-               'display': _('All')}
105  
-        for pk_val, val in self.lookup_choices:
106  
-            yield {'selected': self.lookup_val == smart_unicode(pk_val),
107  
-                   'query_string': cl.get_query_string(
108  
-                                   {self.lookup_kwarg: pk_val},
109  
-                                   [self.lookup_kwarg_isnull]),
110  
-                   'display': val}
111  
-        if isinstance(self.field, models.related.RelatedObject) \
112  
-           and self.field.field.null or hasattr(self.field, 'rel') \
113  
-           and self.field.null:
114  
-            yield {'selected': bool(self.lookup_val_isnull),
115  
-                   'query_string': cl.get_query_string(
116  
-                                   {self.lookup_kwarg_isnull: 'True'},
117  
-                                   [self.lookup_kwarg]),
118  
-                   'display': EMPTY_CHANGELIST_VALUE}
119  
-
120  
-FilterSpec.register(lambda f: (
121  
-        hasattr(f, 'rel') and bool(f.rel) or
122  
-        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
123  
-
124  
-class BooleanFieldFilterSpec(FilterSpec):
125  
-    def __init__(self, f, request, params, model, model_admin,
126  
-                 field_path=None):
127  
-        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
128  
-                                                     model_admin,
129  
-                                                     field_path=field_path)
130  
-        self.lookup_kwarg = '%s__exact' % self.field_path
131  
-        self.lookup_kwarg2 = '%s__isnull' % self.field_path
132  
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
133  
-        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
134  
-
135  
-    def title(self):
136  
-        return self.field.verbose_name
137  
-
138  
-    def choices(self, cl):
139  
-        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
140  
-            yield {'selected': self.lookup_val == v and not self.lookup_val2,
141  
-                   'query_string': cl.get_query_string(
142  
-                                   {self.lookup_kwarg: v},
143  
-                                   [self.lookup_kwarg2]),
144  
-                   'display': k}
145  
-        if isinstance(self.field, models.NullBooleanField):
146  
-            yield {'selected': self.lookup_val2 == 'True',
147  
-                   'query_string': cl.get_query_string(
148  
-                                   {self.lookup_kwarg2: 'True'},
149  
-                                   [self.lookup_kwarg]),
150  
-                   'display': _('Unknown')}
151  
-
152  
-FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
153  
-                              or isinstance(f, models.NullBooleanField),
154  
-                                 BooleanFieldFilterSpec)
155  
-
156  
-class ChoicesFilterSpec(FilterSpec):
157  
-    def __init__(self, f, request, params, model, model_admin,
158  
-                 field_path=None):
159  
-        super(ChoicesFilterSpec, self).__init__(f, request, params, model,
160  
-                                                model_admin,
161  
-                                                field_path=field_path)
162  
-        self.lookup_kwarg = '%s__exact' % self.field_path
163  
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
164  
-
165  
-    def choices(self, cl):
166  
-        yield {'selected': self.lookup_val is None,
167  
-               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
168  
-               'display': _('All')}
169  
-        for k, v in self.field.flatchoices:
170  
-            yield {'selected': smart_unicode(k) == self.lookup_val,
171  
-                    'query_string': cl.get_query_string(
172  
-                                    {self.lookup_kwarg: k}),
173  
-                    'display': v}
174  
-
175  
-FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
176  
-
177  
-class DateFieldFilterSpec(FilterSpec):
178  
-    def __init__(self, f, request, params, model, model_admin,
179  
-                 field_path=None): 
180  
-        super(DateFieldFilterSpec, self).__init__(f, request, params, model,
181  
-                                                  model_admin,
182  
-                                                  field_path=field_path)
183  
-
184  
-        self.field_generic = '%s__' % self.field_path
185  
-
186  
-        self.date_params = dict([(k, v) for k, v in params.items()
187  
-                                 if k.startswith(self.field_generic)])
188  
-
189  
-        today = datetime.date.today()
190  
-        one_week_ago = today - datetime.timedelta(days=7)
191  
-        today_str = isinstance(self.field, models.DateTimeField) \
192  
-                    and today.strftime('%Y-%m-%d 23:59:59') \
193  
-                    or today.strftime('%Y-%m-%d')
194  
-
195  
-        self.links = (
196  
-            (_('Any date'), {}),
197  
-            (_('Today'), {'%s__year' % self.field_path: str(today.year),
198  
-                       '%s__month' % self.field_path: str(today.month),
199  
-                       '%s__day' % self.field_path: str(today.day)}),
200  
-            (_('Past 7 days'), {'%s__gte' % self.field_path:
201  
-                                    one_week_ago.strftime('%Y-%m-%d'),
202  
-                             '%s__lte' % self.field_path: today_str}),
203  
-            (_('This month'), {'%s__year' % self.field_path: str(today.year),
204  
-                             '%s__month' % self.field_path: str(today.month)}),
205  
-            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
206  
-        )
207  
-
208  
-    def title(self):
209  
-        return self.field.verbose_name
210  
-
211  
-    def choices(self, cl):
212  
-        for title, param_dict in self.links:
213  
-            yield {'selected': self.date_params == param_dict,
214  
-                   'query_string': cl.get_query_string(
215  
-                                   param_dict,
216  
-                                   [self.field_generic]),
217  
-                   'display': title}
218  
-
219  
-FilterSpec.register(lambda f: isinstance(f, models.DateField),
220  
-                              DateFieldFilterSpec)
221  
-
222  
-
223  
-# This should be registered last, because it's a last resort. For example,
224  
-# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
225  
-# more appropriate, and the AllValuesFilterSpec won't get used for it.
226  
-class AllValuesFilterSpec(FilterSpec):
227  
-    def __init__(self, f, request, params, model, model_admin,
228  
-                 field_path=None):
229  
-        super(AllValuesFilterSpec, self).__init__(f, request, params, model,
230  
-                                                  model_admin,
231  
-                                                  field_path=field_path)
232  
-        self.lookup_kwarg = self.field_path
233  
-        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
234  
-        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
235  
-        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
236  
-                                                 None)
237  
-        parent_model, reverse_path = reverse_field_path(model, self.field_path)
238  
-        queryset = parent_model._default_manager.all()
239  
-        # optional feature: limit choices base on existing relationships
240  
-        # queryset = queryset.complex_filter(
241  
-        #    {'%s__isnull' % reverse_path: False})
242  
-        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
243  
-        queryset = queryset.filter(limit_choices_to)
244  
-
245  
-        self.lookup_choices = \
246  
-            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
247  
-
248  
-    def title(self):
249  
-        return self.field.verbose_name
250  
-
251  
-    def choices(self, cl):
252  
-        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
253  
-        yield {'selected': self.lookup_val is None
254  
-                           and self.lookup_val_isnull is None,
255  
-               'query_string': cl.get_query_string(
256  
-                               {},
257  
-                               [self.lookup_kwarg, self.lookup_kwarg_isnull]),
258  
-               'display': _('All')}
259  
-        include_none = False
260  
-
261  
-        for val in self.lookup_choices:
262  
-            if val is None:
263  
-                include_none = True
264  
-                continue
265  
-            val = smart_unicode(val)
266  
-
267  
-            yield {'selected': self.lookup_val == val,
268  
-                   'query_string': cl.get_query_string(
269  
-                                   {self.lookup_kwarg: val},
270  
-                                   [self.lookup_kwarg_isnull]),
271  
-                   'display': val}
272  
-        if include_none:
273  
-            yield {'selected': bool(self.lookup_val_isnull),
274  
-                    'query_string': cl.get_query_string(
275  
-                                    {self.lookup_kwarg_isnull: 'True'},
276  
-                                    [self.lookup_kwarg]),
277  
-                    'display': EMPTY_CHANGELIST_VALUE}
278  
-
279  
-FilterSpec.register(lambda f: True, AllValuesFilterSpec)
4  django/contrib/admin/options.py
@@ -1091,7 +1091,7 @@ def changelist_view(self, request, extra_context=None):
1091 1091
         if (actions and request.method == 'POST' and
1092 1092
                 'index' in request.POST and '_save' not in request.POST):
1093 1093
             if selected:
1094  
-                response = self.response_action(request, queryset=cl.get_query_set())
  1094
+                response = self.response_action(request, queryset=cl.get_query_set(request))
1095 1095
                 if response:
1096 1096
                     return response
1097 1097
                 else:
@@ -1107,7 +1107,7 @@ def changelist_view(self, request, extra_context=None):
1107 1107
                 helpers.ACTION_CHECKBOX_NAME in request.POST and
1108 1108
                 'index' not in request.POST and '_save' not in request.POST):
1109 1109
             if selected:
1110  
-                response = self.response_action(request, queryset=cl.get_query_set())
  1110
+                response = self.response_action(request, queryset=cl.get_query_set(request))
1111 1111
                 if response:
1112 1112
                     return response
1113 1113
                 else:
2  django/contrib/admin/templatetags/admin_list.py
@@ -319,7 +319,7 @@ def search_form(cl):
319 319
 
320 320
 @register.inclusion_tag('admin/filter.html')
321 321
 def admin_list_filter(cl, spec):
322  
-    return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
  322
+    return {'title': spec.title, 'choices' : list(spec.choices(cl))}
323 323
 
324 324
 @register.inclusion_tag('admin/actions.html', takes_context=True)
325 325
 def admin_actions(context):
47  django/contrib/admin/validation.py
@@ -3,6 +3,7 @@
3 3
 from django.db.models.fields import FieldDoesNotExist
4 4
 from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
5 5
     _get_foreign_key)
  6
+from django.contrib.admin import ListFilter, FieldListFilter
6 7
 from django.contrib.admin.util import get_fields_from_path, NotRelationField
7 8
 from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
8 9
     HORIZONTAL, VERTICAL)
@@ -54,15 +55,43 @@ def validate(cls, model):
54 55
     # list_filter
55 56
     if hasattr(cls, 'list_filter'):
56 57
         check_isseq(cls, 'list_filter', cls.list_filter)
57  
-        for idx, fpath in enumerate(cls.list_filter):
58  
-            try:
59  
-                get_fields_from_path(model, fpath)
60  
-            except (NotRelationField, FieldDoesNotExist), e:
61  
-                raise ImproperlyConfigured(
62  
-                    "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
63  
-                        cls.__name__, idx, fpath
64  
-                    )
65  
-                )
  58
+        for idx, item in enumerate(cls.list_filter):
  59
+            # There are three options for specifying a filter:
  60
+            #   1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
  61
+            #   2: ('field', SomeFieldListFilter) - a field-based list filter class
  62
+            #   3: SomeListFilter - a non-field list filter class
  63
+            if callable(item) and not isinstance(item, models.Field):
  64
+                # If item is option 3, it should be a ListFilter...
  65
+                if not issubclass(item, ListFilter):
  66
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
  67
+                            " which is not a descendant of ListFilter."
  68
+                            % (cls.__name__, idx, item.__name__))
  69
+                # ...  but not a FieldListFilter.
  70
+                if issubclass(item, FieldListFilter):
  71
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
  72
+                            " which is of type FieldListFilter but is not"
  73
+                            " associated with a field name."
  74
+                            % (cls.__name__, idx, item.__name__))
  75
+            else:
  76
+                try:
  77
+                    # Check for option #2 (tuple)
  78
+                    field, list_filter_class = item
  79
+                except (TypeError, ValueError):
  80
+                    # item is option #1
  81
+                    field = item
  82
+                else:
  83
+                    # item is option #2
  84
+                    if not issubclass(list_filter_class, FieldListFilter):
  85
+                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
  86
+                            " is '%s' which is not of type FieldListFilter."
  87
+                            % (cls.__name__, idx, list_filter_class.__name__))
  88
+                # Validate the field string
  89
+                try:
  90
+                    get_fields_from_path(model, field)
  91
+                except (NotRelationField, FieldDoesNotExist):
  92
+                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
  93
+                            " which does not refer to a Field."
  94
+                            % (cls.__name__, idx, field))
66 95
 
67 96
     # list_per_page = 100
68 97
     if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
107  django/contrib/admin/views/main.py
... ...
@@ -1,13 +1,15 @@
1  
-from django.contrib.admin.filterspecs import FilterSpec
2  
-from django.contrib.admin.options import IncorrectLookupParameters
3  
-from django.contrib.admin.util import quote, get_fields_from_path
  1
+import operator
  2
+
4 3
 from django.core.exceptions import SuspiciousOperation
5 4
 from django.core.paginator import InvalidPage
6 5
 from django.db import models
7 6
 from django.utils.encoding import force_unicode, smart_str
8 7
 from django.utils.translation import ugettext, ugettext_lazy
9 8
 from django.utils.http import urlencode
10  
-import operator
  9
+
  10
+from django.contrib.admin import FieldListFilter
  11
+from django.contrib.admin.options import IncorrectLookupParameters
  12
+from django.contrib.admin.util import quote, get_fields_from_path
11 13
 
12 14
 # The system will display a "Show all" link on the change list only if the
13 15
 # total result count is less than or equal to this setting.
@@ -23,6 +25,9 @@
23 25
 IS_POPUP_VAR = 'pop'
24 26
 ERROR_FLAG = 'e'
25 27
 
  28
+IGNORED_PARAMS = (
  29
+    ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
  30
+
26 31
 # Text to display within change-list table cells if the value is blank.
27 32
 EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
28 33
 
@@ -36,7 +41,9 @@ def field_needs_distinct(field):
36 41
 
37 42
 
38 43
 class ChangeList(object):
39  
-    def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
  44
+    def __init__(self, request, model, list_display, list_display_links,
  45
+            list_filter, date_hierarchy, search_fields, list_select_related,
  46
+            list_per_page, list_editable, model_admin):
40 47
         self.model = model
41 48
         self.opts = model._meta
42 49
         self.lookup_opts = self.opts
@@ -70,20 +77,39 @@ def __init__(self, request, model, list_display, list_display_links, list_filter
70 77
             self.list_editable = list_editable
71 78
         self.order_field, self.order_type = self.get_ordering()
72 79
         self.query = request.GET.get(SEARCH_VAR, '')
73  
-        self.query_set = self.get_query_set()
  80
+        self.query_set = self.get_query_set(request)
74 81
         self.get_results(request)
75  
-        self.title = (self.is_popup and ugettext('Select %s') % force_unicode(self.opts.verbose_name) or ugettext('Select %s to change') % force_unicode(self.opts.verbose_name))
76  
-        self.filter_specs, self.has_filters = self.get_filters(request)
  82
+        if self.is_popup:
  83
+            title = ugettext('Select %s')
  84
+        else:
  85
+            title = ugettext('Select %s to change')
  86
+        self.title = title % force_unicode(self.opts.verbose_name)
77 87
         self.pk_attname = self.lookup_opts.pk.attname
78 88
 
79  
-    def get_filters(self, request):
  89
+    def get_filters(self, request, use_distinct=False):
80 90
         filter_specs = []
  91
+        cleaned_params, use_distinct = self.get_lookup_params(use_distinct)
81 92
         if self.list_filter:
82  
-            for filter_name in self.list_filter:
83  
-                field = get_fields_from_path(self.model, filter_name)[-1]
84  
-                spec = FilterSpec.create(field, request, self.params,
85  
-                                         self.model, self.model_admin,
86  
-                                         field_path=filter_name)
  93
+            for list_filer in self.list_filter:
  94
+                if callable(list_filer):
  95
+                    # This is simply a custom list filter class.
  96
+                    spec = list_filer(request, cleaned_params,
  97
+                        self.model, self.model_admin)
  98
+                else:
  99
+                    field_path = None
  100
+                    try:
  101
+                        # This is custom FieldListFilter class for a given field.
  102
+                        field, field_list_filter_class = list_filer
  103
+                    except (TypeError, ValueError):
  104
+                        # This is simply a field name, so use the default
  105
+                        # FieldListFilter class that has been registered for
  106
+                        # the type of the given field.
  107
+                        field, field_list_filter_class = list_filer, FieldListFilter.create
  108
+                    if not isinstance(field, models.Field):
  109
+                        field_path = field
  110
+                        field = get_fields_from_path(self.model, field_path)[-1]
  111
+                    spec = field_list_filter_class(field, request, cleaned_params,
  112
+                        self.model, self.model_admin, field_path=field_path)
87 113
                 if spec and spec.has_output():
88 114
                     filter_specs.append(spec)
89 115
         return filter_specs, bool(filter_specs)
@@ -175,14 +201,13 @@ def get_ordering(self):
175 201
             order_type = params[ORDER_TYPE_VAR]
176 202
         return order_field, order_type
177 203
 
178  
-    def get_query_set(self):
179  
-        use_distinct = False
180  
-
181  
-        qs = self.root_query_set
  204
+    def get_lookup_params(self, use_distinct=False):
182 205
         lookup_params = self.params.copy() # a dictionary of the query string
183  
-        for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR):
184  
-            if i in lookup_params:
185  
-                del lookup_params[i]
  206
+
  207
+        for ignored in IGNORED_PARAMS:
  208
+            if ignored in lookup_params:
  209
+                del lookup_params[ignored]
  210
+
186 211
         for key, value in lookup_params.items():
187 212
             if not isinstance(key, str):
188 213
                 # 'key' will be used as a keyword argument later, so Python
@@ -195,10 +220,11 @@ def get_query_set(self):
195 220
                 # instance
196 221
                 field_name = key.split('__', 1)[0]
197 222
                 try:
198  
-                    f = self.lookup_opts.get_field_by_name(field_name)[0]
  223
+                    field = self.lookup_opts.get_field_by_name(field_name)[0]
  224
+                    use_distinct = field_needs_distinct(field)
199 225
                 except models.FieldDoesNotExist:
200  
-                    raise IncorrectLookupParameters
201  
-                use_distinct = field_needs_distinct(f)
  226
+                    # It might be a custom NonFieldFilter
  227
+                    pass
202 228
 
203 229
             # if key ends with __in, split parameter into separate values
204 230
             if key.endswith('__in'):
@@ -214,11 +240,28 @@ def get_query_set(self):
214 240
                 lookup_params[key] = value
215 241
 
216 242
             if not self.model_admin.lookup_allowed(key, value):
217  
-                raise SuspiciousOperation(
218  
-                    "Filtering by %s not allowed" % key
219  
-                )
  243
+                raise SuspiciousOperation("Filtering by %s not allowed" % key)
  244
+
  245
+        return lookup_params, use_distinct
  246
+
  247
+    def get_query_set(self, request):
  248
+        lookup_params, use_distinct = self.get_lookup_params(use_distinct=False)
  249
+        self.filter_specs, self.has_filters = self.get_filters(request, use_distinct)
  250
+
  251
+        # Let every list filter modify the qs and params to its liking
  252
+        qs = self.root_query_set
  253
+        for filter_spec in self.filter_specs:
  254
+            new_qs = filter_spec.queryset(request, qs)
  255
+            if new_qs is not None:
  256
+                qs = new_qs
  257
+                for param in filter_spec.used_params():
  258
+                    try:
  259
+                        del lookup_params[param]
  260
+                    except KeyError:
  261
+                        pass
220 262
 
221  
-        # Apply lookup parameters from the query string.
  263
+        # Apply the remaining lookup parameters from the query string (i.e.
  264
+        # those that haven't already been processed by the filters).
222 265
         try:
223 266
             qs = qs.filter(**lookup_params)
224 267
         # Naked except! Because we don't have any other way of validating "params".
@@ -226,8 +269,8 @@ def get_query_set(self):
226 269
         # values are not in the correct type, so we might get FieldError, ValueError,
227 270
         # ValicationError, or ? from a custom field that raises yet something else
228 271
         # when handed impossible data.
229  
-        except:
230  
-            raise IncorrectLookupParameters
  272
+        except Exception, e:
  273
+            raise IncorrectLookupParameters(e)
231 274
 
232 275
         # Use select_related() if one of the list_display options is a field
233 276
         # with a relationship and the provided queryset doesn't already have
@@ -238,11 +281,11 @@ def get_query_set(self):
238 281
             else:
239 282
                 for field_name in self.list_display:
240 283
                     try:
241  
-                        f = self.lookup_opts.get_field(field_name)
  284
+                        field = self.lookup_opts.get_field(field_name)
242 285
                     except models.FieldDoesNotExist:
243 286
                         pass
244 287
                     else:
245  
-                        if isinstance(f.rel, models.ManyToOneRel):
  288
+                        if isinstance(field.rel, models.ManyToOneRel):
246 289
                             qs = qs.select_related()
247 290
                             break
248 291
 
2  django/db/models/related.py
@@ -27,7 +27,7 @@ def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH,
123  docs/ref/contrib/admin/index.txt
@@ -525,30 +525,113 @@ subclass::
525 525
 
526 526
 .. attribute:: ModelAdmin.list_filter
527 527
 
528  
-    Set ``list_filter`` to activate filters in the right sidebar of the change
529  
-    list page of the admin. This should be a list of field names, and each
530  
-    specified field should be either a ``BooleanField``, ``CharField``,
531  
-    ``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
532  
-
533  
-    This example, taken from the ``django.contrib.auth.models.User`` model,
534  
-    shows how both ``list_display`` and ``list_filter`` work::
  528
+    .. versionchanged:: 1.4
535 529
 
536  
-        class UserAdmin(admin.ModelAdmin):
537  
-            list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
538  
-            list_filter = ('is_staff', 'is_superuser')
539  
-
540  
-    The above code results in an admin change list page that looks like this:
  530
+    Set ``list_filter`` to activate filters in the right sidebar of the change
  531
+    list page of the admin, as illustrated in the following screenshot:
541 532
 
542 533
         .. image:: _images/users_changelist.png
543 534
 
544  
-    (This example also has ``search_fields`` defined. See below.)
545  
-
546  
-    .. versionadded:: 1.3
547  
-
548  
-    Fields in ``list_filter`` can also span relations using the ``__`` lookup::
549  
-
550  
-        class UserAdminWithLookup(UserAdmin):
551  
-            list_filter = ('groups__name')
  535
+    ``list_filter`` should be a list of elements, where each element should be
  536
+    of one of the following types:
  537
+
  538
+        * a field name, where the specified field should be either a
  539
+          ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
  540
+          ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example::
  541
+
  542
+              class PersonAdmin(ModelAdmin):
  543
+                  list_filter = ('is_staff', 'company')
  544
+
  545
+          .. versionadded:: 1.3
  546
+
  547
+          Field names in ``list_filter`` can also span relations
  548
+          using the ``__`` lookup, for example::
  549
+
  550
+              class PersonAdmin(UserAdmin):
  551
+                  list_filter = ('company__name',)
  552
+
  553
+        * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`,
  554
+          which you need to provide the ``title`` and ``parameter_name``
  555
+          attributes to and override the ``lookups`` and ``queryset`` methods,
  556
+          e.g.::
  557
+
  558
+               from django.db.models import Q
  559
+               from django.utils.translation import ugettext_lazy as _
  560
+
  561
+               from django.contrib.admin import SimpleListFilter
  562
+
  563
+               class DecadeBornListFilter(SimpleListFilter):
  564
+                   # Human-readable title which will be displayed in the
  565
+                   # right admin sidebar just above the filter options.
  566
+                   title = _('decade born')
  567
+
  568
+                   # Parameter for the filter that will be used in the URL query.
  569
+                   parameter_name = 'decade'
  570
+
  571
+                   def lookups(self, request):
  572
+                       """
  573
+                       Returns a list of tuples. The first element in each
  574
+                       tuple is the coded value for the option that will
  575
+                       appear in the URL query. The second element is the
  576
+                       human-readable name for the option that will appear
  577
+                       in the right sidebar.
  578
+                       """
  579
+                       return (
  580
+                           ('80s', 'in the eighties'),
  581
+                           ('other', 'other'),
  582
+                       )
  583
+
  584
+                   def queryset(self, request, queryset):
  585
+                       """
  586
+                       Returns the filtered queryset based on the value
  587
+                       provided in the query string and retrievable via
  588
+                       ``value()``.
  589
+                       """
  590
+                       # Compare the requested value (either '80s' or 'other')
  591
+                       # to decide how to filter the queryset.
  592
+                       if self.value() == '80s':
  593
+                           return queryset.filter(birthday__year__gte=1980,