Ticket #5833: 5833.custom-filterspecs.8.diff

File 5833.custom-filterspecs.8.diff, 75.9 KB (added by Jannis Leidel, 14 years ago)

Extended patch with further API refinements based on thorough patch review

  • django/contrib/admin/__init__.py

    From 270cd4e0938db0023e0cadfc48ca4b4a0b068d41 Mon Sep 17 00:00:00 2001
    From: Jannis Leidel <jannis@leidel.info>
    Date: Mon, 2 May 2011 18:58:29 +0200
    Subject: [PATCH] Fixed #5833 -- Extended admin's FilterSpecs to be easier to override.
    
    ---
     django/contrib/admin/__init__.py                  |    4 +
     django/contrib/admin/filterspecs.py               |  497 +++++++++++++--------
     django/contrib/admin/templatetags/admin_list.py   |    2 +-
     django/contrib/admin/validation.py                |   47 ++-
     django/contrib/admin/views/main.py                |  106 +++--
     django/db/models/related.py                       |    2 +-
     docs/ref/contrib/admin/index.txt                  |  123 +++++-
     tests/regressiontests/admin_filterspecs/models.py |   16 +-
     tests/regressiontests/admin_filterspecs/tests.py  |  348 ++++++++++++---
     tests/regressiontests/modeladmin/tests.py         |   69 +++-
     10 files changed, 886 insertions(+), 328 deletions(-)
    
    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..3e69c37 100644
    a b from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME  
    44from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    55from django.contrib.admin.options import StackedInline, TabularInline
    66from django.contrib.admin.sites import AdminSite, site
     7from django.contrib.admin.filterspecs import (ListFilterBase,
     8        SimpleListFilter, FieldListFilter, BooleanFieldListFilter,
     9        RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter,
     10        AllValuesFieldListFilter)
    711
    812
    913def autodiscover():
  • django/contrib/admin/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    index 965b32b..a871c70 100644
    a b Filters are specified in models with the "list_filter" option.  
    55Each filter subclass knows how to display a filter for a field that passes a
    66certain test -- e.g. being a DateField or ForeignKey.
    77"""
     8import datetime
    89
    910from 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
     11from django.core.exceptions import ImproperlyConfigured
     12from django.utils.encoding import smart_unicode
     13from django.utils.translation import ugettext_lazy as _
     14
     15from django.contrib.admin.util import (get_model_from_relation,
     16    reverse_field_path, get_limit_choices_to_from_path)
     17
     18class ListFilterBase(object):
     19    title = None  # Human-readable title to appear in the right sidebar.
    1720
    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
     21    def __init__(self, request, params, model, model_admin):
    2322        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
     55class SimpleListFilter(ListFilterBase):
     56    # The parameter that should be used in the query string for that filter.
     57    # Defaults to the title, slugified.
     58    parameter_name = None
     59
     60    def __init__(self, request, params, model, model_admin):
     61        super(SimpleListFilter, self).__init__(
     62            request, params, model, model_admin)
     63        if self.parameter_name is None:
     64            raise ImproperlyConfigured(
     65                "The list filter '%s' does not specify "
     66                "a 'parameter_name'." % self.__class__.__name__)
     67        self.lookup_choices = self.lookups(request)
     68
     69    def has_output(self):
     70        return len(self.lookup_choices) > 0
     71
     72    def value(self):
     73        """
     74        Returns the value given in the query string for this filter,
     75        if any. Returns None otherwise.
     76        """
     77        return self.params.get(self.parameter_name, None)
     78
     79    def lookups(self, request):
     80        """
     81        Must be overriden to return a list of tuples (value, verbose value)
     82        """
     83        raise NotImplementedError
     84
     85    def used_params(self):
     86        return [self.parameter_name]
     87
     88    def choices(self, cl):
     89        yield {
     90            'selected': self.value() is None,
     91            'query_string': cl.get_query_string({}, [self.parameter_name]),
     92            'display': _('All'),
     93        }
     94        for lookup, title in self.lookup_choices:
     95            yield {
     96                'selected': self.value() == lookup,
     97                'query_string': cl.get_query_string({
     98                    self.parameter_name: lookup,
     99                }, []),
     100                'display': title,
     101            }
     102
     103
     104class FieldListFilter(ListFilterBase):
     105    _field_list_filters = []
     106    _take_priority_index = 0
     107
     108    def __init__(self, field, request, params, model, model_admin, field_path):
     109        self.field = field
    24110        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)
     111        self.title = field_path
     112        super(FieldListFilter, self).__init__(request, params, model, model_admin)
    41113
    42114    def has_output(self):
    43115        return True
    44116
    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,
     117    def queryset(self, request, queryset):
     118        for p in self.used_params():
     119            if p in self.params:
     120                return queryset.filter(**{p: self.params[p]})
     121
     122    @classmethod
     123    def register(cls, test, list_filter_class, take_priority=False):
     124        if take_priority:
     125            # This is to allow overriding the default filters for certain types
     126            # of fields with some custom filters. The first found in the list
     127            # is used in priority.
     128            cls._field_list_filters.insert(
     129                cls._take_priority_index, (test, list_filter_class))
     130            cls._take_priority_index += 1
     131        else:
     132            cls._field_list_filters.append((test, list_filter_class))
     133
     134    @classmethod
     135    def create(cls, field, request, params, model, model_admin, field_path):
     136        for test, list_filter_class in cls._field_list_filters:
     137            if not test(field):
     138                continue
     139            return list_filter_class(field, request, params,
     140                model, model_admin, field_path=field_path)
     141
     142
     143class RelatedFieldListFilter(FieldListFilter):
     144    def __init__(self, field, request, params, model, model_admin, field_path):
     145        super(RelatedFieldListFilter, self).__init__(
     146            field, request, params, model, model_admin, field_path)
     147
     148        other_model = get_model_from_relation(field)
     149        if isinstance(field, (models.ManyToManyField,
    72150                          models.related.RelatedObject)):
    73151            # no direct field on this model, get name from other model
    74152            self.lookup_title = other_model._meta.verbose_name
    75153        else:
    76             self.lookup_title = f.verbose_name # use field name
     154            self.lookup_title = field.verbose_name # use field name
    77155        rel_name = other_model._meta.pk.name
    78156        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
    79157        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
    80158        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    81159        self.lookup_val_isnull = request.GET.get(
    82160                                      self.lookup_kwarg_isnull, None)
    83         self.lookup_choices = f.get_choices(include_blank=False)
     161        self.lookup_choices = field.get_choices(include_blank=False)
     162        self.title = self.lookup_title
    84163
    85164    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:
     165        if (isinstance(self.field, models.related.RelatedObject)
     166                and self.field.field.null or hasattr(self.field, 'rel')
     167                    and self.field.null):
    89168            extra = 1
    90169        else:
    91170            extra = 0
    92171        return len(self.lookup_choices) + extra > 1
    93172
    94     def title(self):
    95         return self.lookup_title
     173    def used_params(self):
     174        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
    96175
    97176    def choices(self, cl):
    98177        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')}
     178        yield {
     179            'selected': self.lookup_val is None and not self.lookup_val_isnull,
     180            'query_string': cl.get_query_string({},
     181                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
     182            'display': _('All'),
     183        }
    105184        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: (
     185            yield {
     186                'selected': self.lookup_val == smart_unicode(pk_val),
     187                'query_string': cl.get_query_string({
     188                    self.lookup_kwarg: pk_val,
     189                }, [self.lookup_kwarg_isnull]),
     190                'display': val,
     191            }
     192        if (isinstance(self.field, models.related.RelatedObject)
     193                and self.field.field.null or hasattr(self.field, 'rel')
     194                    and self.field.null):
     195            yield {
     196                'selected': bool(self.lookup_val_isnull),
     197                'query_string': cl.get_query_string({
     198                    self.lookup_kwarg_isnull: 'True',
     199                }, [self.lookup_kwarg]),
     200                'display': EMPTY_CHANGELIST_VALUE,
     201            }
     202
     203FieldListFilter.register(lambda f: (
    121204        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)
     205        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
     206
     207class 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)
    130211        self.lookup_kwarg = '%s__exact' % self.field_path
    131212        self.lookup_kwarg2 = '%s__isnull' % self.field_path
    132213        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133214        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134215
    135     def title(self):
    136         return self.field.verbose_name
     216    def used_params(self):
     217        return [self.lookup_kwarg, self.lookup_kwarg2]
    137218
    138219    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}
     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            }
    145231        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)
     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
     240FieldListFilter.register(lambda f: isinstance(f,
     241    (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
     242
     243
     244class 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)
    162248        self.lookup_kwarg = '%s__exact' % self.field_path
    163         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     249        self.lookup_val = request.GET.get(self.lookup_kwarg)
     250
     251    def used_params(self):
     252        return [self.lookup_kwarg]
    164253
    165254    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)
     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
     267FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
     268
     269
     270class 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)
    183274
    184275        self.field_generic = '%s__' % self.field_path
    185 
    186276        self.date_params = dict([(k, v) for k, v in params.items()
    187277                                 if k.startswith(self.field_generic)])
    188278
    189279        today = datetime.date.today()
    190280        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')
     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
    194290
    195291        self.links = (
    196292            (_('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)})
     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            }),
    206309        )
    207310
    208     def title(self):
    209         return self.field.verbose_name
     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)
    210328
    211329    def choices(self, cl):
    212330        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}
     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            }
    218337
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     338FieldListFilter.register(
     339    lambda f: isinstance(f, models.DateField), DateFieldListFilter)
    221340
    222341
    223342# 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)
     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.
     345class 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)
    232349        self.lookup_kwarg = self.field_path
    233350        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
    234351        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    235         self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
    236                                                  None)
     352        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None)
    237353        parent_model, reverse_path = reverse_field_path(model, self.field_path)
    238354        queryset = parent_model._default_manager.all()
    239355        # optional feature: limit choices base on existing relationships
    class AllValuesFilterSpec(FilterSpec):  
    242358        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
    243359        queryset = queryset.filter(limit_choices_to)
    244360
    245         self.lookup_choices = \
    246             queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
     361        self.lookup_choices = queryset.distinct(
     362            ).order_by(field.name).values_list(field.name, flat=True)
    247363
    248     def title(self):
    249         return self.field.verbose_name
     364    def used_params(self):
     365        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
    250366
    251367    def choices(self, cl):
    252368        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')}
     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        }
    259376        include_none = False
    260 
    261377        for val in self.lookup_choices:
    262378            if val is None:
    263379                include_none = True
    264380                continue
    265381            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}
     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            }
    272389        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)
     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
     398FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  • django/contrib/admin/templatetags/admin_list.py

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
    index 5e199ce..b72c0be 100644
    a b def search_form(cl):  
    319319
    320320@register.inclusion_tag('admin/filter.html')
    321321def 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))}
    323323
    324324@register.inclusion_tag('admin/actions.html', takes_context=True)
    325325def admin_actions(context):
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 159afa4..f989cf3 100644
    a b from django.db import models  
    33from django.db.models.fields import FieldDoesNotExist
    44from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
    55    _get_foreign_key)
     6from django.contrib.admin.filterspecs import ListFilterBase, FieldListFilter
    67from django.contrib.admin.util import get_fields_from_path, NotRelationField
    78from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
    89    HORIZONTAL, VERTICAL)
    def validate(cls, model):  
    5455    # list_filter
    5556    if hasattr(cls, 'list_filter'):
    5657        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 ListFilterBase...
     65                if not issubclass(item, ListFilterBase):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not a descendant of ListFilterBase."
     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))
    6695
    6796    # list_per_page = 100
    6897    if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
  • django/contrib/admin/views/main.py

    diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
    index 0cfc43d..4413466 100644
    a b  
    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
     1import operator
     2
    43from django.core.exceptions import SuspiciousOperation
    54from django.core.paginator import InvalidPage
    65from django.db import models
    76from django.utils.encoding import force_unicode, smart_str
    87from django.utils.translation import ugettext, ugettext_lazy
    98from django.utils.http import urlencode
    10 import operator
     9
     10from django.contrib.admin.filterspecs import FieldListFilter
     11from django.contrib.admin.options import IncorrectLookupParameters
     12from django.contrib.admin.util import quote, get_fields_from_path
    1113
    1214# The system will display a "Show all" link on the change list only if the
    1315# total result count is less than or equal to this setting.
    TO_FIELD_VAR = 't'  
    2325IS_POPUP_VAR = 'pop'
    2426ERROR_FLAG = 'e'
    2527
     28IGNORED_PARAMS = (
     29    ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
     30
    2631# Text to display within change-list table cells if the value is blank.
    2732EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
    2833
    def field_needs_distinct(field):  
    3641
    3742
    3843class 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):
     47        self.request = request
    4048        self.model = model
    4149        self.opts = model._meta
    4250        self.lookup_opts = self.opts
    class ChangeList(object):  
    7280        self.query = request.GET.get(SEARCH_VAR, '')
    7381        self.query_set = self.get_query_set()
    7482        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)
     83        if self.is_popup:
     84            title = ugettext('Select %s')
     85        else:
     86            title = ugettext('Select %s to change')
     87        self.title = title % force_unicode(self.opts.verbose_name)
    7788        self.pk_attname = self.lookup_opts.pk.attname
    7889
    79     def get_filters(self, request):
     90    def get_filters(self, request, use_distinct=False):
    8091        filter_specs = []
     92        cleaned_params, use_distinct = self.get_lookup_params(use_distinct)
    8193        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)
     94            for list_filer in self.list_filter:
     95                if callable(list_filer):
     96                    # This is simply a custom list filter class.
     97                    spec = list_filer(request, cleaned_params,
     98                        self.model, self.model_admin)
     99                else:
     100                    field_path = None
     101                    try:
     102                        # This is custom FieldListFilter class for a given field.
     103                        field, field_list_filter_class = list_filer
     104                    except (TypeError, ValueError):
     105                        # This is simply a field name, so use the default
     106                        # FieldListFilter class that has been registered for
     107                        # the type of the given field.
     108                        field, field_list_filter_class = list_filer, FieldListFilter.create
     109                    if not isinstance(field, models.Field):
     110                        field_path = field
     111                        field = get_fields_from_path(self.model, field_path)[-1]
     112                    spec = field_list_filter_class(field, request, cleaned_params,
     113                        self.model, self.model_admin, field_path=field_path)
    87114                if spec and spec.has_output():
    88115                    filter_specs.append(spec)
    89116        return filter_specs, bool(filter_specs)
    class ChangeList(object):  
    175202            order_type = params[ORDER_TYPE_VAR]
    176203        return order_field, order_type
    177204
    178     def get_query_set(self):
    179         use_distinct = False
    180 
    181         qs = self.root_query_set
     205    def get_lookup_params(self, use_distinct=False):
    182206        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]
     207
     208        for ignored in IGNORED_PARAMS:
     209            if ignored in lookup_params:
     210                del lookup_params[ignored]
     211
    186212        for key, value in lookup_params.items():
    187213            if not isinstance(key, str):
    188214                # 'key' will be used as a keyword argument later, so Python
    class ChangeList(object):  
    195221                # instance
    196222                field_name = key.split('__', 1)[0]
    197223                try:
    198                     f = self.lookup_opts.get_field_by_name(field_name)[0]
     224                    field = self.lookup_opts.get_field_by_name(field_name)[0]
     225                    use_distinct = field_needs_distinct(field)
    199226                except models.FieldDoesNotExist:
    200                     raise IncorrectLookupParameters
    201                 use_distinct = field_needs_distinct(f)
     227                    # It might be a custom NonFieldFilter
     228                    pass
    202229
    203230            # if key ends with __in, split parameter into separate values
    204231            if key.endswith('__in'):
    class ChangeList(object):  
    214241                lookup_params[key] = value
    215242
    216243            if not self.model_admin.lookup_allowed(key, value):
    217                 raise SuspiciousOperation(
    218                     "Filtering by %s not allowed" % key
    219                 )
     244                raise SuspiciousOperation("Filtering by %s not allowed" % key)
     245
     246        return lookup_params, use_distinct
     247
     248    def get_query_set(self):
     249        lookup_params, use_distinct = self.get_lookup_params(use_distinct=False)
     250        self.filter_specs, self.has_filters = self.get_filters(self.request, use_distinct)
     251
     252        # Let every list filter modify the qs and params to its liking
     253        qs = self.root_query_set
     254        for filter_spec in self.filter_specs:
     255            new_qs = filter_spec.queryset(self.request, qs)
     256            if new_qs is not None:
     257                qs = new_qs
     258                for param in filter_spec.used_params():
     259                    try:
     260                        del lookup_params[param]
     261                    except KeyError:
     262                        pass
    220263
    221         # Apply lookup parameters from the query string.
     264        # Apply the remaining lookup parameters from the query string (i.e.
     265        # those that haven't already been processed by the filters).
    222266        try:
    223267            qs = qs.filter(**lookup_params)
    224268        # Naked except! Because we don't have any other way of validating "params".
    class ChangeList(object):  
    226270        # values are not in the correct type, so we might get FieldError, ValueError,
    227271        # ValicationError, or ? from a custom field that raises yet something else
    228272        # when handed impossible data.
    229         except:
    230             raise IncorrectLookupParameters
     273        except Exception, e:
     274            raise IncorrectLookupParameters(e)
    231275
    232276        # Use select_related() if one of the list_display options is a field
    233277        # with a relationship and the provided queryset doesn't already have
    class ChangeList(object):  
    238282            else:
    239283                for field_name in self.list_display:
    240284                    try:
    241                         f = self.lookup_opts.get_field(field_name)
     285                        field = self.lookup_opts.get_field(field_name)
    242286                    except models.FieldDoesNotExist:
    243287                        pass
    244288                    else:
    245                         if isinstance(f.rel, models.ManyToOneRel):
     289                        if isinstance(field.rel, models.ManyToOneRel):
    246290                            qs = qs.select_related()
    247291                            break
    248292
  • django/db/models/related.py

    diff --git a/django/db/models/related.py b/django/db/models/related.py
    index 7734230..90995d7 100644
    a b class RelatedObject(object):  
    2727        as SelectField choices for this field.
    2828
    2929        Analogue of django.db.models.fields.Field.get_choices, provided
    30         initially for utilisation by RelatedFilterSpec.
     30        initially for utilisation by RelatedFieldListFilter.
    3131        """
    3232        first_choice = include_blank and blank_choice or []
    3333        queryset = self.model._default_manager.all()
  • docs/ref/contrib/admin/index.txt

    diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
    index 633c53f..31adf46 100644
    a b subclass::  
    525525
    526526.. attribute:: ModelAdmin.list_filter
    527527
    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
    535529
    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:
    541532
    542533        .. image:: _images/users_changelist.png
    543534
    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          where you need to provide a few attributes and override a few
     555          methods::
     556
     557              from django.db.models import Q
     558              from django.utils.translation import ugettext_lazy as _
     559
     560              from django.contrib.admin import SimpleListFilter
     561
     562              class DecadeBornListFilter(SimpleListFilter):
     563                  # Human-readable title which will be displayed in the
     564                  # right sidebar just above the filter options.
     565                  verbose_name = _('decade born')
     566
     567                  # Parameter for the filter that will be used in the url query.
     568                  # Providing this attribute is optional. If it is not provided then a
     569                  # slugified version of the title will automatically be used instead
     570                  # (that is, 'decade-born' in this example).
     571                  parameter_name = 'decade'
     572
     573                  def lookups(self, *args, **kwargs):
     574                      """
     575                      Returns a list of tuples. The first element in each tuple
     576                      is the coded value for the option that will appear in the
     577                      url query. The second element is the human-readable name
     578                      for the option that will appear in the right sidebar.
     579                      """
     580                      return (
     581                          ('80s', 'in the eighties'),
     582                          ('other', 'other'),
     583                      )
     584
     585                  def queryset(self, queryset, *args, **kwargs):
     586                      """
     587                      Returns the filtered queryset based on the value provided
     588                      in the query string and retrievable via `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,
     594                                                 birthday__year__lte=1989)
     595                      if self.value() == 'other':
     596                          return queryset.filter(Q(year__lte=1979) |
     597                                                 Q(year__gte=1990))
     598
     599              class PersonAdmin(ModelAdmin):
     600                  list_filter = (DecadeBornListFilter,)
     601
     602          .. note::
     603
     604              As a convenience, the ``HttpRequest`` object is passed to the
     605              filter's methods, for example::
     606
     607                  class AuthDecadeBornListFilter(DecadeBornListFilter):
     608
     609                      def lookups(self, request, *args, **kwargs):
     610                          if request.user.is_authenticated():
     611                              return (
     612                                  ('80s', 'in the eighties'),
     613                                  ('other', 'other'),
     614                              )
     615                          else:
     616                              return (
     617                                  ('90s', 'in the nineties'),
     618                              )
     619
     620        * a tuple, where the first element is a field name and the second
     621          element is a class inheriting from
     622          :mod:`django.contrib.admin.FieldListFilter`, for example::
     623
     624              from django.contrib.admin import BooleanFieldListFilter
     625
     626              class PersonAdmin(ModelAdmin):
     627                  list_filter = (
     628                      ('is_staff', BooleanFieldListFilter),
     629                  )
     630
     631          .. note::
     632
     633              The ``FieldListFilter`` API is currently considered internal
     634              and prone to refactoring.
    552635
    553636.. attribute:: ModelAdmin.list_per_page
    554637
  • tests/regressiontests/admin_filterspecs/models.py

    diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
    index 5b284c7..80d54c7 100644
    a b from django.db import models  
    22from django.contrib.auth.models import User
    33
    44class Book(models.Model):
    5     title = models.CharField(max_length=25)
     5    title = models.CharField(max_length=50)
    66    year = models.PositiveIntegerField(null=True, blank=True)
    77    author = models.ForeignKey(User, related_name='books_authored', blank=True, null=True)
    88    contributors = models.ManyToManyField(User, related_name='books_contributed', blank=True, null=True)
     9    is_best_seller = models.NullBooleanField(default=0)
     10    date_registered = models.DateField(null=True)
    911
    1012    def __unicode__(self):
    1113        return self.title
    12 
    13 class BoolTest(models.Model):
    14     NO = False
    15     YES = True
    16     YES_NO_CHOICES = (
    17         (NO, 'no'),
    18         (YES, 'yes')
    19     )
    20     completed = models.BooleanField(
    21         default=NO,
    22         choices=YES_NO_CHOICES
    23     )
  • tests/regressiontests/admin_filterspecs/tests.py

    diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
    index 8b9e734..b3c5562 100644
    a b  
     1import datetime
     2import calendar
     3
    14from django.contrib.auth.admin import UserAdmin
    25from django.test import TestCase
    36from django.test.client import RequestFactory
    from django.contrib.auth.models import User  
    58from django.contrib import admin
    69from django.contrib.admin.views.main import ChangeList
    710from django.utils.encoding import force_unicode
     11from django.contrib.admin.filterspecs import (SimpleListFilter,
     12    BooleanFieldListFilter, FieldListFilter)
    813
    9 from models import Book, BoolTest
     14from models import Book
    1015
    1116def select_by(dictlist, key, value):
    1217    return [x for x in dictlist if x[key] == value][0]
    1318
    14 class FilterSpecsTests(TestCase):
    1519
    16     def setUp(self):
    17         # Users
    18         self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
    19         self.bob = User.objects.create_user('bob', 'bob@example.com')
    20         lisa = User.objects.create_user('lisa', 'lisa@example.com')
     20class DecadeListFilterBase(SimpleListFilter):
     21
     22    def lookups(self, request):
     23        return (
     24            (u'the 90s', u'the 1990\'s'),
     25            (u'the 00s', u'the 2000\'s'),
     26            (u'other', u'other decades'),
     27        )
     28
     29    def queryset(self, request, queryset):
     30        decade = self.value()
     31        if decade == u'the 90s':
     32            return queryset.filter(year__gte=1990, year__lte=1999)
     33        if decade == u'the 00s':
     34            return queryset.filter(year__gte=2000, year__lte=2009)
     35        return queryset
     36
     37class DecadeListFilterWithTitle(DecadeListFilterBase):
     38    title = 'publication decade'
     39    parameter_name = 'publication-decade'
    2140
    22         #Books
    23         self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred)
    24         self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob)
    25         gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
    26         gipsy_book.contributors = [self.bob, lisa]
    27         gipsy_book.save()
     41class DecadeListFilterWithParamName(DecadeListFilterBase):
     42    title = 'another publication decade'
     43    parameter_name = 'blah'
    2844
    29         # BoolTests
    30         self.trueTest = BoolTest.objects.create(completed=True)
    31         self.falseTest = BoolTest.objects.create(completed=False)
     45class ListFiltersTests(TestCase):
     46
     47    def setUp(self):
     48        self.today = datetime.date.today()
     49        self.one_week_ago = self.today - datetime.timedelta(days=7)
    3250
    3351        self.request_factory = RequestFactory()
    3452
     53        # Users
     54        self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
     55        self.bob = User.objects.create_user('bob', 'bob@example.com')
     56        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
     57
     58        # Books
     59        self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred, is_best_seller=True, date_registered=self.today)
     60        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False)
     61        self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today)
     62        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago)
     63        self.gipsy_book.contributors = [self.bob, self.lisa]
     64        self.gipsy_book.save()
    3565
    3666    def get_changelist(self, request, model, modeladmin):
    3767        return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
    3868            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    3969            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
    4070
    41     def test_AllValuesFilterSpec(self):
     71    def test_DateFieldListFilter(self):
     72        modeladmin = BookAdmin(Book, admin.site)
     73
     74        request = self.request_factory.get('/')
     75        changelist = self.get_changelist(request, Book, modeladmin)
     76
     77        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     78                                                 'date_registered__month': self.today.month,
     79                                                 'date_registered__day': self.today.day})
     80        changelist = self.get_changelist(request, Book, modeladmin)
     81
     82        # Make sure the correct queryset is returned
     83        queryset = changelist.get_query_set()
     84        self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     85
     86        # Make sure the correct choice is selected
     87        filterspec = changelist.get_filters(request)[0][4]
     88        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     89        choice = select_by(filterspec.choices(changelist), "display", "Today")
     90        self.assertEqual(choice['selected'], True)
     91        self.assertEqual(choice['query_string'], '?date_registered__day=%s'
     92                                                 '&date_registered__month=%s'
     93                                                 '&date_registered__year=%s'
     94                                                % (self.today.day, self.today.month, self.today.year))
     95
     96        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     97                                                 'date_registered__month': self.today.month})
     98        changelist = self.get_changelist(request, Book, modeladmin)
     99
     100        # Make sure the correct queryset is returned
     101        queryset = changelist.get_query_set()
     102        if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month):
     103            # In case one week ago is in the same month.
     104            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     105        else:
     106            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     107
     108        # Make sure the correct choice is selected
     109        filterspec = changelist.get_filters(request)[0][4]
     110        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     111        choice = select_by(filterspec.choices(changelist), "display", "This month")
     112        self.assertEqual(choice['selected'], True)
     113        self.assertEqual(choice['query_string'], '?date_registered__month=%s'
     114                                                 '&date_registered__year=%s'
     115                                                % (self.today.month, self.today.year))
     116
     117        request = self.request_factory.get('/', {'date_registered__year': self.today.year})
     118        changelist = self.get_changelist(request, Book, modeladmin)
     119
     120        # Make sure the correct queryset is returned
     121        queryset = changelist.get_query_set()
     122        if self.today.year == self.one_week_ago.year:
     123            # In case one week ago is in the same year.
     124            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     125        else:
     126            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     127
     128        # Make sure the correct choice is selected
     129        filterspec = changelist.get_filters(request)[0][4]
     130        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     131        choice = select_by(filterspec.choices(changelist), "display", "This year")
     132        self.assertEqual(choice['selected'], True)
     133        self.assertEqual(choice['query_string'], '?date_registered__year=%s'
     134                                                % (self.today.year))
     135
     136        request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'),
     137                                                 'date_registered__lte': self.today.strftime('%Y-%m-%d')})
     138        changelist = self.get_changelist(request, Book, modeladmin)
     139
     140        # Make sure the correct queryset is returned
     141        queryset = changelist.get_query_set()
     142        self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     143
     144        # Make sure the correct choice is selected
     145        filterspec = changelist.get_filters(request)[0][4]
     146        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     147        choice = select_by(filterspec.choices(changelist), "display", "Past 7 days")
     148        self.assertEqual(choice['selected'], True)
     149        self.assertEqual(choice['query_string'], '?date_registered__gte=%s'
     150                                                 '&date_registered__lte=%s'
     151                                                % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d')))
     152
     153    def test_AllValuesFieldListFilter(self):
    42154        modeladmin = BookAdmin(Book, admin.site)
    43155
    44156        request = self.request_factory.get('/', {'year__isnull': 'True'})
    45157        changelist = self.get_changelist(request, Book, modeladmin)
    46158
    47         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     159        # Make sure the correct queryset is returned
    48160        queryset = changelist.get_query_set()
     161        self.assertEqual(list(queryset), [self.django_book])
    49162
    50163        # Make sure the last choice is None and is selected
    51164        filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
     165        self.assertEqual(force_unicode(filterspec.title), u'year')
    53166        choices = list(filterspec.choices(changelist))
    54167        self.assertEqual(choices[-1]['selected'], True)
    55168        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    class FilterSpecsTests(TestCase):  
    59172
    60173        # Make sure the correct choice is selected
    61174        filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
     175        self.assertEqual(force_unicode(filterspec.title), u'year')
    63176        choices = list(filterspec.choices(changelist))
    64177        self.assertEqual(choices[2]['selected'], True)
    65178        self.assertEqual(choices[2]['query_string'], '?year=2002')
    66179
    67     def test_RelatedFilterSpec_ForeignKey(self):
     180    def test_RelatedFieldListFilter_ForeignKey(self):
    68181        modeladmin = BookAdmin(Book, admin.site)
    69182
    70183        request = self.request_factory.get('/', {'author__isnull': 'True'})
    71         changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
    72             modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    73             modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
     184        changelist = self.get_changelist(request, Book, modeladmin)
    74185
    75         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     186        # Make sure the correct queryset is returned
    76187        queryset = changelist.get_query_set()
     188        self.assertEqual(list(queryset), [self.gipsy_book])
    77189
    78190        # Make sure the last choice is None and is selected
    79191        filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
     192        self.assertEqual(force_unicode(filterspec.title), u'author')
    81193        choices = list(filterspec.choices(changelist))
    82194        self.assertEqual(choices[-1]['selected'], True)
    83195        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    class FilterSpecsTests(TestCase):  
    87199
    88200        # Make sure the correct choice is selected
    89201        filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     202        self.assertEqual(force_unicode(filterspec.title), u'author')
    91203        # order of choices depends on User model, which has no order
    92204        choice = select_by(filterspec.choices(changelist), "display", "alfred")
    93205        self.assertEqual(choice['selected'], True)
    94206        self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
    95207
    96     def test_RelatedFilterSpec_ManyToMany(self):
     208    def test_RelatedFieldListFilter_ManyToMany(self):
    97209        modeladmin = BookAdmin(Book, admin.site)
    98210
    99211        request = self.request_factory.get('/', {'contributors__isnull': 'True'})
    100212        changelist = self.get_changelist(request, Book, modeladmin)
    101213
    102         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     214        # Make sure the correct queryset is returned
    103215        queryset = changelist.get_query_set()
     216        self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book])
    104217
    105218        # Make sure the last choice is None and is selected
    106219        filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
     220        self.assertEqual(force_unicode(filterspec.title), u'user')
    108221        choices = list(filterspec.choices(changelist))
    109222        self.assertEqual(choices[-1]['selected'], True)
    110223        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    class FilterSpecsTests(TestCase):  
    114227
    115228        # Make sure the correct choice is selected
    116229        filterspec = changelist.get_filters(request)[0][2]
    117         self.assertEqual(force_unicode(filterspec.title()), u'user')
     230        self.assertEqual(force_unicode(filterspec.title), u'user')
    118231        choice = select_by(filterspec.choices(changelist), "display", "bob")
    119232        self.assertEqual(choice['selected'], True)
    120233        self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
    121234
    122 
    123     def test_RelatedFilterSpec_reverse_relationships(self):
     235    def test_RelatedFieldListFilter_reverse_relationships(self):
    124236        modeladmin = CustomUserAdmin(User, admin.site)
    125237
    126238        # FK relationship -----
    127239        request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
    128240        changelist = self.get_changelist(request, User, modeladmin)
    129241
    130         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     242        # Make sure the correct queryset is returned
    131243        queryset = changelist.get_query_set()
     244        self.assertEqual(list(queryset), [self.lisa])
    132245
    133246        # Make sure the last choice is None and is selected
    134247        filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
     248        self.assertEqual(force_unicode(filterspec.title), u'book')
    136249        choices = list(filterspec.choices(changelist))
    137250        self.assertEqual(choices[-1]['selected'], True)
    138251        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    class FilterSpecsTests(TestCase):  
    142255
    143256        # Make sure the correct choice is selected
    144257        filterspec = changelist.get_filters(request)[0][0]
    145         self.assertEqual(force_unicode(filterspec.title()), u'book')
     258        self.assertEqual(force_unicode(filterspec.title), u'book')
    146259        choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title)
    147260        self.assertEqual(choice['selected'], True)
    148261        self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
    class FilterSpecsTests(TestCase):  
    151264        request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
    152265        changelist = self.get_changelist(request, User, modeladmin)
    153266
    154         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     267        # Make sure the correct queryset is returned
    155268        queryset = changelist.get_query_set()
     269        self.assertEqual(list(queryset), [self.alfred])
    156270
    157271        # Make sure the last choice is None and is selected
    158272        filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
     273        self.assertEqual(force_unicode(filterspec.title), u'book')
    160274        choices = list(filterspec.choices(changelist))
    161275        self.assertEqual(choices[-1]['selected'], True)
    162276        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    class FilterSpecsTests(TestCase):  
    166280
    167281        # Make sure the correct choice is selected
    168282        filterspec = changelist.get_filters(request)[0][1]
    169         self.assertEqual(force_unicode(filterspec.title()), u'book')
     283        self.assertEqual(force_unicode(filterspec.title), u'book')
    170284        choice = select_by(filterspec.choices(changelist), "display", self.django_book.title)
    171285        self.assertEqual(choice['selected'], True)
    172286        self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
    173287
    174     def test_BooleanFilterSpec(self):
    175         modeladmin = BoolTestAdmin(BoolTest, admin.site)
     288    def test_BooleanFieldListFilter(self):
     289        modeladmin = BookAdmin(Book, admin.site)
     290        self.verify_BooleanFieldListFilter(modeladmin)
     291
     292    def test_BooleanFieldListFilter_Tuple(self):
     293        modeladmin = BookAdmin(Book, admin.site)
     294        self.verify_BooleanFieldListFilter(modeladmin)
    176295
     296    def verify_BooleanFieldListFilter(self, modeladmin):
    177297        request = self.request_factory.get('/')
    178         changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links,
    179             modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    180             modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
     298        changelist = self.get_changelist(request, Book, modeladmin)
     299
     300        request = self.request_factory.get('/', {'is_best_seller__exact': 0})
     301        changelist = self.get_changelist(request, Book, modeladmin)
    181302
    182         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     303        # Make sure the correct queryset is returned
    183304        queryset = changelist.get_query_set()
     305        self.assertEqual(list(queryset), [self.bio_book])
    184306
    185         # Make sure the last choice is None and is selected
    186         filterspec = changelist.get_filters(request)[0][0]
    187         self.assertEqual(force_unicode(filterspec.title()), u'completed')
     307        # Make sure the correct choice is selected
     308        filterspec = changelist.get_filters(request)[0][3]
     309        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     310        choice = select_by(filterspec.choices(changelist), "display", "No")
     311        self.assertEqual(choice['selected'], True)
     312        self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
     313
     314        request = self.request_factory.get('/', {'is_best_seller__exact': 1})
     315        changelist = self.get_changelist(request, Book, modeladmin)
     316
     317        # Make sure the correct queryset is returned
     318        queryset = changelist.get_query_set()
     319        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     320
     321        # Make sure the correct choice is selected
     322        filterspec = changelist.get_filters(request)[0][3]
     323        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     324        choice = select_by(filterspec.choices(changelist), "display", "Yes")
     325        self.assertEqual(choice['selected'], True)
     326        self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
     327
     328        request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
     329        changelist = self.get_changelist(request, Book, modeladmin)
     330
     331        # Make sure the correct queryset is returned
     332        queryset = changelist.get_query_set()
     333        self.assertEqual(list(queryset), [self.django_book])
     334
     335        # Make sure the correct choice is selected
     336        filterspec = changelist.get_filters(request)[0][3]
     337        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     338        choice = select_by(filterspec.choices(changelist), "display", "Unknown")
     339        self.assertEqual(choice['selected'], True)
     340        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
     341
     342    def test_SimpleListFilter(self):
     343        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
     344
     345        # Make sure that the first option is 'All' ---------------------------
     346
     347        request = self.request_factory.get('/', {})
     348        changelist = self.get_changelist(request, Book, modeladmin)
     349
     350        # Make sure the correct queryset is returned
     351        queryset = changelist.get_query_set()
     352        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
     353
     354        # Make sure the correct choice is selected
     355        filterspec = changelist.get_filters(request)[0][1]
     356        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     357        choices = list(filterspec.choices(changelist))
     358        self.assertEqual(choices[0]['display'], u'All')
     359        self.assertEqual(choices[0]['selected'], True)
     360        self.assertEqual(choices[0]['query_string'], '?')
     361
     362        # Make sure that one can override the query parameter name -----------
     363
     364        request = self.request_factory.get('/', {'blah': 'the 90s'})
     365        changelist = self.get_changelist(request, Book, modeladmin)
     366
     367        # Make sure the correct choice is selected
     368        filterspec = changelist.get_filters(request)[0][2]
     369        self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
     370        choices = list(filterspec.choices(changelist))
     371        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     372        self.assertEqual(choices[1]['selected'], True)
     373        self.assertEqual(choices[1]['query_string'], '?blah=the+90s')
     374
     375        # Look for books in the 1990s ----------------------------------------
     376
     377        request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
     378        changelist = self.get_changelist(request, Book, modeladmin)
     379
     380        # Make sure the correct queryset is returned
     381        queryset = changelist.get_query_set()
     382        self.assertEqual(list(queryset), [self.bio_book])
     383
     384        # Make sure the correct choice is selected
     385        filterspec = changelist.get_filters(request)[0][1]
     386        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
    188387        choices = list(filterspec.choices(changelist))
    189         self.assertEqual(choices[-1]['selected'], False)
    190         self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
     388        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     389        self.assertEqual(choices[1]['selected'], True)
     390        self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
     391
     392        # Look for books in the 2000s ----------------------------------------
     393
     394        request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
     395        changelist = self.get_changelist(request, Book, modeladmin)
    191396
    192         request = self.request_factory.get('/', {'completed__exact': 1})
    193         changelist = self.get_changelist(request, BoolTest, modeladmin)
     397        # Make sure the correct queryset is returned
     398        queryset = changelist.get_query_set()
     399        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
    194400
    195401        # Make sure the correct choice is selected
     402        filterspec = changelist.get_filters(request)[0][1]
     403        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     404        choices = list(filterspec.choices(changelist))
     405        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     406        self.assertEqual(choices[2]['selected'], True)
     407        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
     408
     409        # Combine multiple filters -------------------------------------------
     410
     411        request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
     412        changelist = self.get_changelist(request, Book, modeladmin)
     413
     414        # Make sure the correct queryset is returned
     415        queryset = changelist.get_query_set()
     416        self.assertEqual(list(queryset), [self.djangonaut_book])
     417
     418        # Make sure the correct choices are selected
     419        filterspec = changelist.get_filters(request)[0][1]
     420        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     421        choices = list(filterspec.choices(changelist))
     422        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     423        self.assertEqual(choices[2]['selected'], True)
     424        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     425
    196426        filterspec = changelist.get_filters(request)[0][0]
    197         self.assertEqual(force_unicode(filterspec.title()), u'completed')
    198         # order of choices depends on User model, which has no order
    199         choice = select_by(filterspec.choices(changelist), "display", "Yes")
     427        self.assertEqual(force_unicode(filterspec.title), u'author')
     428        choice = select_by(filterspec.choices(changelist), "display", "alfred")
    200429        self.assertEqual(choice['selected'], True)
    201         self.assertEqual(choice['query_string'], '?completed__exact=1')
     430        self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
    202431
    203432class CustomUserAdmin(UserAdmin):
    204433    list_filter = ('books_authored', 'books_contributed')
    205434
    206435class BookAdmin(admin.ModelAdmin):
    207     list_filter = ('year', 'author', 'contributors')
     436    list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
    208437    order_by = '-id'
    209438
    210 class BoolTestAdmin(admin.ModelAdmin):
    211     list_filter = ('completed',)
     439class DecadeFilterBookAdmin(admin.ModelAdmin):
     440    list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName)
     441    order_by = '-id'
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..b6c62d9 100644
    a b from datetime import date  
    22
    33from django import forms
    44from django.conf import settings
    5 from django.contrib.admin.options import ModelAdmin, TabularInline, \
    6     HORIZONTAL, VERTICAL
     5from django.contrib.admin.options import (ModelAdmin, TabularInline,
     6    HORIZONTAL, VERTICAL)
    77from django.contrib.admin.sites import AdminSite
    88from django.contrib.admin.validation import validate
    99from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
     10from django.contrib.admin.filterspecs import (SimpleListFilter,
     11     BooleanFieldListFilter)
    1012from django.core.exceptions import ImproperlyConfigured
    1113from django.forms.models import BaseModelFormSet
    1214from django.forms.widgets import Select
    1315from django.test import TestCase
    1416from django.utils import unittest
    1517
    16 from models import Band, Concert, ValidationTestModel, \
    17     ValidationTestInlineModel
     18from models import (Band, Concert, ValidationTestModel,
     19    ValidationTestInlineModel)
    1820
    1921
    2022# None of the following tests really depend on the content of the request,
    class ValidationTests(unittest.TestCase):  
    851853            ValidationTestModel,
    852854        )
    853855
     856        class RandomClass(object):
     857            pass
     858
     859        class ValidationTestModelAdmin(ModelAdmin):
     860            list_filter = (RandomClass,)
     861
     862        self.assertRaisesRegexp(
     863            ImproperlyConfigured,
     864            "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of ListFilterBase.",
     865            validate,
     866            ValidationTestModelAdmin,
     867            ValidationTestModel,
     868        )
     869
     870        class ValidationTestModelAdmin(ModelAdmin):
     871            list_filter = (('is_active', RandomClass),)
     872
     873        self.assertRaisesRegexp(
     874            ImproperlyConfigured,
     875            "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.",
     876            validate,
     877            ValidationTestModelAdmin,
     878            ValidationTestModel,
     879        )
     880
     881        class AwesomeFilter(SimpleListFilter):
     882            def get_title(self):
     883                return 'awesomeness'
     884            def get_choices(self, request):
     885                return (('bit', 'A bit awesome'), ('very', 'Very awesome'), )
     886            def get_query_set(self, cl, qs):
     887                return qs
     888
     889        class ValidationTestModelAdmin(ModelAdmin):
     890            list_filter = (('is_active', AwesomeFilter),)
     891
     892        self.assertRaisesRegexp(
     893            ImproperlyConfigured,
     894            "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.",
     895            validate,
     896            ValidationTestModelAdmin,
     897            ValidationTestModel,
     898        )
     899
     900        class ValidationTestModelAdmin(ModelAdmin):
     901            list_filter = (BooleanFieldListFilter,)
     902
     903        self.assertRaisesRegexp(
     904            ImproperlyConfigured,
     905            "'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.",
     906            validate,
     907            ValidationTestModelAdmin,
     908            ValidationTestModel,
     909        )
     910
     911        # Valid declarations below -----------
     912
    854913        class ValidationTestModelAdmin(ModelAdmin):
    855             list_filter = ('is_active',)
     914            list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
    856915
    857916        validate(ValidationTestModelAdmin, ValidationTestModel)
    858917
Back to Top