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

File 5833.custom-filterspecs.9.diff, 81.4 KB (added by Julien Phalip, 8 years ago)
  • django/contrib/admin/__init__.py

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..6a0d6b1 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.filters import (ListFilterBase,
     8        SimpleListFilter, FieldListFilter, BooleanFieldListFilter,
     9        RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter,
     10        AllValuesFieldListFilter)
    711
    812
    913def autodiscover():
  • new file django/contrib/admin/filters.py

    diff --git a/django/contrib/admin/filters.py b/django/contrib/admin/filters.py
    new file mode 100644
    index 0000000..bb3bef8
    - +  
     1"""
     2FilterSpec encapsulates the logic for displaying filters in the Django admin.
     3Filters are specified in models with the "list_filter" option.
     4
     5Each filter subclass knows how to display a filter for a field that passes a
     6certain test -- e.g. being a DateField or ForeignKey.
     7"""
     8import datetime
     9
     10from django.db import models
     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.
     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
     55class SimpleListFilter(ListFilterBase):
     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
     103class FieldListFilter(ListFilterBase):
     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
     142class 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
     202FieldListFilter.register(lambda f: (
     203        hasattr(f, 'rel') and bool(f.rel) or
     204        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
     205
     206class BooleanFieldListFilter(FieldListFilter):
     207    def __init__(self, field, request, params, model, model_admin, field_path):
     208        super(BooleanFieldListFilter, self).__init__(field,
     209            request, params, model, model_admin, field_path)
     210        self.lookup_kwarg = '%s__exact' % self.field_path
     211        self.lookup_kwarg2 = '%s__isnull' % self.field_path
     212        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     213        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
     214
     215    def used_params(self):
     216        return [self.lookup_kwarg, self.lookup_kwarg2]
     217
     218    def choices(self, cl):
     219        for lookup, title in (
     220                (None, _('All')),
     221                ('1', _('Yes')),
     222                ('0', _('No'))):
     223            yield {
     224                'selected': self.lookup_val == lookup and not self.lookup_val2,
     225                'query_string': cl.get_query_string({
     226                        self.lookup_kwarg: lookup,
     227                    }, [self.lookup_kwarg2]),
     228                'display': title,
     229            }
     230        if isinstance(self.field, models.NullBooleanField):
     231            yield {
     232                'selected': self.lookup_val2 == 'True',
     233                'query_string': cl.get_query_string({
     234                        self.lookup_kwarg2: 'True',
     235                    }, [self.lookup_kwarg]),
     236                'display': _('Unknown'),
     237            }
     238
     239FieldListFilter.register(lambda f: isinstance(f,
     240    (models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
     241
     242
     243class ChoicesFieldListFilter(FieldListFilter):
     244    def __init__(self, field, request, params, model, model_admin, field_path):
     245        super(ChoicesFieldListFilter, self).__init__(
     246            field, request, params, model, model_admin, field_path)
     247        self.lookup_kwarg = '%s__exact' % self.field_path
     248        self.lookup_val = request.GET.get(self.lookup_kwarg)
     249
     250    def used_params(self):
     251        return [self.lookup_kwarg]
     252
     253    def choices(self, cl):
     254        yield {
     255            'selected': self.lookup_val is None,
     256            'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
     257            'display': _('All')
     258        }
     259        for lookup, title in self.field.flatchoices:
     260            yield {
     261                'selected': smart_unicode(lookup) == self.lookup_val,
     262                'query_string': cl.get_query_string({self.lookup_kwarg: lookup}),
     263                'display': title,
     264            }
     265
     266FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
     267
     268
     269class DateFieldListFilter(FieldListFilter):
     270    def __init__(self, field, request, params, model, model_admin, field_path):
     271        super(DateFieldListFilter, self).__init__(
     272            field, request, params, model, model_admin, field_path)
     273
     274        self.field_generic = '%s__' % self.field_path
     275        self.date_params = dict([(k, v) for k, v in params.items()
     276                                 if k.startswith(self.field_generic)])
     277
     278        today = datetime.date.today()
     279        one_week_ago = today - datetime.timedelta(days=7)
     280        today_str = (isinstance(self.field, models.DateTimeField)
     281                        and today.strftime('%Y-%m-%d 23:59:59')
     282                        or today.strftime('%Y-%m-%d'))
     283
     284        self.lookup_kwarg_year = '%s__year' % self.field_path
     285        self.lookup_kwarg_month = '%s__month' % self.field_path
     286        self.lookup_kwarg_day = '%s__day' % self.field_path
     287        self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
     288        self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
     289
     290        self.links = (
     291            (_('Any date'), {}),
     292            (_('Today'), {
     293                self.lookup_kwarg_year: str(today.year),
     294                self.lookup_kwarg_month: str(today.month),
     295                self.lookup_kwarg_day: str(today.day),
     296            }),
     297            (_('Past 7 days'), {
     298                self.lookup_kwarg_past_7_days_gte: one_week_ago.strftime('%Y-%m-%d'),
     299                self.lookup_kwarg_past_7_days_lte: today_str,
     300            }),
     301            (_('This month'), {
     302                self.lookup_kwarg_year: str(today.year),
     303                self.lookup_kwarg_month: str(today.month),
     304            }),
     305            (_('This year'), {
     306                self.lookup_kwarg_year: str(today.year),
     307            }),
     308        )
     309
     310    def used_params(self):
     311        return [
     312            self.lookup_kwarg_year, self.lookup_kwarg_month, self.lookup_kwarg_day,
     313            self.lookup_kwarg_past_7_days_gte, self.lookup_kwarg_past_7_days_lte
     314        ]
     315
     316    def queryset(self, request, queryset):
     317        """
     318        Override the default behaviour since there can be multiple query
     319        string parameters used for the same date filter (e.g. year + month).
     320        """
     321        query_dict = {}
     322        for p in self.used_params():
     323            if p in self.params:
     324                query_dict[p] = self.params[p]
     325        if len(query_dict):
     326            return queryset.filter(**query_dict)
     327
     328    def choices(self, cl):
     329        for title, param_dict in self.links:
     330            yield {
     331                'selected': self.date_params == param_dict,
     332                'query_string': cl.get_query_string(
     333                    param_dict, [self.field_generic]),
     334                'display': title,
     335            }
     336
     337FieldListFilter.register(
     338    lambda f: isinstance(f, models.DateField), DateFieldListFilter)
     339
     340
     341# This should be registered last, because it's a last resort. For example,
     342# if a field is eligible to use the BooleanFieldListFilter, that'd be much
     343# more appropriate, and the AllValuesFieldListFilter won't get used for it.
     344class AllValuesFieldListFilter(FieldListFilter):
     345    def __init__(self, field, request, params, model, model_admin, field_path):
     346        super(AllValuesFieldListFilter, self).__init__(
     347            field, request, params, model, model_admin, field_path)
     348        self.lookup_kwarg = self.field_path
     349        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
     350        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     351        self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None)
     352        parent_model, reverse_path = reverse_field_path(model, self.field_path)
     353        queryset = parent_model._default_manager.all()
     354        # optional feature: limit choices base on existing relationships
     355        # queryset = queryset.complex_filter(
     356        #    {'%s__isnull' % reverse_path: False})
     357        limit_choices_to = get_limit_choices_to_from_path(model, field_path)
     358        queryset = queryset.filter(limit_choices_to)
     359
     360        self.lookup_choices = queryset.distinct(
     361            ).order_by(field.name).values_list(field.name, flat=True)
     362
     363    def used_params(self):
     364        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
     365
     366    def choices(self, cl):
     367        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
     368        yield {
     369            'selected': (self.lookup_val is None
     370                and self.lookup_val_isnull is None),
     371            'query_string': cl.get_query_string({},
     372                [self.lookup_kwarg, self.lookup_kwarg_isnull]),
     373            'display': _('All'),
     374        }
     375        include_none = False
     376        for val in self.lookup_choices:
     377            if val is None:
     378                include_none = True
     379                continue
     380            val = smart_unicode(val)
     381            yield {
     382                'selected': self.lookup_val == val,
     383                'query_string': cl.get_query_string({
     384                    self.lookup_kwarg: val,
     385                }, [self.lookup_kwarg_isnull]),
     386                'display': val,
     387            }
     388        if include_none:
     389            yield {
     390                'selected': bool(self.lookup_val_isnull),
     391                'query_string': cl.get_query_string({
     392                    self.lookup_kwarg_isnull: 'True',
     393                }, [self.lookup_kwarg]),
     394                'display': EMPTY_CHANGELIST_VALUE,
     395            }
     396
     397FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  • deleted file django/contrib/admin/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    deleted file mode 100644
    index 965b32b..0000000
    + -  
    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)
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 098eda5..787f856 100644
    a b class ModelAdmin(BaseModelAdmin): 
    10911091        if (actions and request.method == 'POST' and
    10921092                'index' in request.POST and '_save' not in request.POST):
    10931093            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))
    10951095                if response:
    10961096                    return response
    10971097                else:
    class ModelAdmin(BaseModelAdmin): 
    11071107                helpers.ACTION_CHECKBOX_NAME in request.POST and
    11081108                'index' not in request.POST and '_save' not in request.POST):
    11091109            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))
    11111111                if response:
    11121112                    return response
    11131113                else:
  • 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..73b400c 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 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..d5f401c 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 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):
    4047        self.model = model
    4148        self.opts = model._meta
    4249        self.lookup_opts = self.opts
    class ChangeList(object): 
    7077            self.list_editable = list_editable
    7178        self.order_field, self.order_type = self.get_ordering()
    7279        self.query = request.GET.get(SEARCH_VAR, '')
    73         self.query_set = self.get_query_set()
     80        self.query_set = self.get_query_set(request)
    7481        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)
    7787        self.pk_attname = self.lookup_opts.pk.attname
    7888
    79     def get_filters(self, request):
     89    def get_filters(self, request, use_distinct=False):
    8090        filter_specs = []
     91        cleaned_params, use_distinct = self.get_lookup_params(use_distinct)
    8192        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)
    87113                if spec and spec.has_output():
    88114                    filter_specs.append(spec)
    89115        return filter_specs, bool(filter_specs)
    class ChangeList(object): 
    175201            order_type = params[ORDER_TYPE_VAR]
    176202        return order_field, order_type
    177203
    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):
    182205        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
    186211        for key, value in lookup_params.items():
    187212            if not isinstance(key, str):
    188213                # 'key' will be used as a keyword argument later, so Python
    class ChangeList(object): 
    195220                # instance
    196221                field_name = key.split('__', 1)[0]
    197222                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)
    199225                except models.FieldDoesNotExist:
    200                     raise IncorrectLookupParameters
    201                 use_distinct = field_needs_distinct(f)
     226                    # It might be a custom NonFieldFilter
     227                    pass
    202228
    203229            # if key ends with __in, split parameter into separate values
    204230            if key.endswith('__in'):
    class ChangeList(object): 
    214240                lookup_params[key] = value
    215241
    216242            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
    220262
    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).
    222265        try:
    223266            qs = qs.filter(**lookup_params)
    224267        # Naked except! Because we don't have any other way of validating "params".
    class ChangeList(object): 
    226269        # values are not in the correct type, so we might get FieldError, ValueError,
    227270        # ValicationError, or ? from a custom field that raises yet something else
    228271        # when handed impossible data.
    229         except:
    230             raise IncorrectLookupParameters
     272        except Exception, e:
     273            raise IncorrectLookupParameters(e)
    231274
    232275        # Use select_related() if one of the list_display options is a field
    233276        # with a relationship and the provided queryset doesn't already have
    class ChangeList(object): 
    238281            else:
    239282                for field_name in self.list_display:
    240283                    try:
    241                         f = self.lookup_opts.get_field(field_name)
     284                        field = self.lookup_opts.get_field(field_name)
    242285                    except models.FieldDoesNotExist:
    243286                        pass
    244287                    else:
    245                         if isinstance(f.rel, models.ManyToOneRel):
     288                        if isinstance(field.rel, models.ManyToOneRel):
    246289                            qs = qs.select_related()
    247290                            break
    248291
  • 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..7178037 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                  parameter_name = 'decade'
     569
     570                  def lookups(self, request):
     571                      """
     572                      Returns a list of tuples. The first element in each tuple
     573                      is the coded value for the option that will appear in the
     574                      url query. The second element is the human-readable name
     575                      for the option that will appear in the right sidebar.
     576                      """
     577                      return (
     578                          ('80s', 'in the eighties'),
     579                          ('other', 'other'),
     580                      )
     581
     582                  def queryset(self, request, queryset):
     583                      """
     584                      Returns the filtered queryset based on the value provided
     585                      in the query string and retrievable via `value()`.
     586                      """
     587                      # Compare the requested value (either '80s' or 'other')
     588                      # to decide how to filter the queryset.
     589                      if self.value() == '80s':
     590                          return queryset.filter(birthday__year__gte=1980,
     591                                                 birthday__year__lte=1989)
     592                      if self.value() == 'other':
     593                          return queryset.filter(Q(year__lte=1979) |
     594                                                 Q(year__gte=1990))
     595
     596              class PersonAdmin(ModelAdmin):
     597                  list_filter = (DecadeBornListFilter,)
     598
     599          .. note::
     600
     601              As a convenience, the ``HttpRequest`` object is passed to the
     602              filter's methods, for example::
     603
     604                  class AuthDecadeBornListFilter(DecadeBornListFilter):
     605
     606                      def lookups(self, request):
     607                          if request.user.is_authenticated():
     608                              return (
     609                                  ('80s', 'in the eighties'),
     610                                  ('other', 'other'),
     611                              )
     612                          else:
     613                              return (
     614                                  ('90s', 'in the nineties'),
     615                              )
     616
     617        * a tuple, where the first element is a field name and the second
     618          element is a class inheriting from
     619          :mod:`django.contrib.admin.FieldListFilter`, for example::
     620
     621              from django.contrib.admin import BooleanFieldListFilter
     622
     623              class PersonAdmin(ModelAdmin):
     624                  list_filter = (
     625                      ('is_staff', BooleanFieldListFilter),
     626                  )
     627
     628          .. note::
     629
     630              The ``FieldListFilter`` API is currently considered internal
     631              and prone to refactoring.
    552632
    553633.. attribute:: ModelAdmin.list_per_page
    554634
  • 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..7cec6db 100644
    a b  
     1from __future__ import with_statement
     2
     3import datetime
     4import calendar
     5
    16from django.contrib.auth.admin import UserAdmin
    27from django.test import TestCase
    38from django.test.client import RequestFactory
    from django.contrib.auth.models import User 
    510from django.contrib import admin
    611from django.contrib.admin.views.main import ChangeList
    712from django.utils.encoding import force_unicode
     13from django.core.exceptions import ImproperlyConfigured
     14from django.contrib.admin import (SimpleListFilter,
     15    BooleanFieldListFilter, FieldListFilter)
    816
    9 from models import Book, BoolTest
     17from models import Book
    1018
    1119def select_by(dictlist, key, value):
    1220    return [x for x in dictlist if x[key] == value][0]
    1321
    14 class FilterSpecsTests(TestCase):
    1522
    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')
     23class DecadeListFilterBase(SimpleListFilter):
     24
     25    def lookups(self, request):
     26        return (
     27            (u'the 90s', u'the 1990\'s'),
     28            (u'the 00s', u'the 2000\'s'),
     29            (u'other', u'other decades'),
     30        )
     31
     32    def queryset(self, request, queryset):
     33        decade = self.value()
     34        if decade == u'the 90s':
     35            return queryset.filter(year__gte=1990, year__lte=1999)
     36        if decade == u'the 00s':
     37            return queryset.filter(year__gte=2000, year__lte=2009)
     38        return queryset
     39
     40class DecadeListFilterWithTitleAndParameter(DecadeListFilterBase):
     41    title = 'publication decade'
     42    parameter_name = 'publication-decade'
     43
     44class DecadeListFilterWithoutTitle(DecadeListFilterBase):
     45    parameter_name = 'publication-decade'
     46
     47class DecadeListFilterWithoutParameter(DecadeListFilterBase):
     48    title = 'publication decade'
     49   
     50class CustomUserAdmin(UserAdmin):
     51    list_filter = ('books_authored', 'books_contributed')
     52
     53class BookAdmin(admin.ModelAdmin):
     54    list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
     55    order_by = '-id'
    2156
    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()
     57class DecadeFilterBookAdmin(admin.ModelAdmin):
     58    list_filter = ('author', DecadeListFilterWithTitleAndParameter)
     59    order_by = '-id'
     60   
     61class DecadeFilterBookAdminWithoutTitle(admin.ModelAdmin):
     62    list_filter = (DecadeListFilterWithoutTitle,)
     63   
     64class DecadeFilterBookAdminWithoutParameter(admin.ModelAdmin):
     65    list_filter = (DecadeListFilterWithoutParameter,)
     66   
     67class ListFiltersTests(TestCase):
    2868
    29         # BoolTests
    30         self.trueTest = BoolTest.objects.create(completed=True)
    31         self.falseTest = BoolTest.objects.create(completed=False)
     69    def setUp(self):
     70        self.today = datetime.date.today()
     71        self.one_week_ago = self.today - datetime.timedelta(days=7)
    3272
    3373        self.request_factory = RequestFactory()
    3474
     75        # Users
     76        self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
     77        self.bob = User.objects.create_user('bob', 'bob@example.com')
     78        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
     79
     80        # Books
     81        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)
     82        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False)
     83        self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today)
     84        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago)
     85        self.gipsy_book.contributors = [self.bob, self.lisa]
     86        self.gipsy_book.save()
    3587
    3688    def get_changelist(self, request, model, modeladmin):
    3789        return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
    3890            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    3991            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
    4092
    41     def test_AllValuesFilterSpec(self):
     93    def test_DateFieldListFilter(self):
     94        modeladmin = BookAdmin(Book, admin.site)
     95
     96        request = self.request_factory.get('/')
     97        changelist = self.get_changelist(request, Book, modeladmin)
     98
     99        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     100                                                 'date_registered__month': self.today.month,
     101                                                 'date_registered__day': self.today.day})
     102        changelist = self.get_changelist(request, Book, modeladmin)
     103
     104        # Make sure the correct queryset is returned
     105        queryset = changelist.get_query_set(request)
     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", "Today")
     112        self.assertEqual(choice['selected'], True)
     113        self.assertEqual(choice['query_string'], '?date_registered__day=%s'
     114                                                 '&date_registered__month=%s'
     115                                                 '&date_registered__year=%s'
     116                                                % (self.today.day, self.today.month, self.today.year))
     117
     118        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     119                                                 'date_registered__month': self.today.month})
     120        changelist = self.get_changelist(request, Book, modeladmin)
     121
     122        # Make sure the correct queryset is returned
     123        queryset = changelist.get_query_set(request)
     124        if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month):
     125            # In case one week ago is in the same month.
     126            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     127        else:
     128            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     129
     130        # Make sure the correct choice is selected
     131        filterspec = changelist.get_filters(request)[0][4]
     132        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     133        choice = select_by(filterspec.choices(changelist), "display", "This month")
     134        self.assertEqual(choice['selected'], True)
     135        self.assertEqual(choice['query_string'], '?date_registered__month=%s'
     136                                                 '&date_registered__year=%s'
     137                                                % (self.today.month, self.today.year))
     138
     139        request = self.request_factory.get('/', {'date_registered__year': self.today.year})
     140        changelist = self.get_changelist(request, Book, modeladmin)
     141
     142        # Make sure the correct queryset is returned
     143        queryset = changelist.get_query_set(request)
     144        if self.today.year == self.one_week_ago.year:
     145            # In case one week ago is in the same year.
     146            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     147        else:
     148            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     149
     150        # Make sure the correct choice is selected
     151        filterspec = changelist.get_filters(request)[0][4]
     152        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     153        choice = select_by(filterspec.choices(changelist), "display", "This year")
     154        self.assertEqual(choice['selected'], True)
     155        self.assertEqual(choice['query_string'], '?date_registered__year=%s'
     156                                                % (self.today.year))
     157
     158        request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'),
     159                                                 'date_registered__lte': self.today.strftime('%Y-%m-%d')})
     160        changelist = self.get_changelist(request, Book, modeladmin)
     161
     162        # Make sure the correct queryset is returned
     163        queryset = changelist.get_query_set(request)
     164        self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     165
     166        # Make sure the correct choice is selected
     167        filterspec = changelist.get_filters(request)[0][4]
     168        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     169        choice = select_by(filterspec.choices(changelist), "display", "Past 7 days")
     170        self.assertEqual(choice['selected'], True)
     171        self.assertEqual(choice['query_string'], '?date_registered__gte=%s'
     172                                                 '&date_registered__lte=%s'
     173                                                % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d')))
     174
     175    def test_AllValuesFieldListFilter(self):
    42176        modeladmin = BookAdmin(Book, admin.site)
    43177
    44178        request = self.request_factory.get('/', {'year__isnull': 'True'})
    45179        changelist = self.get_changelist(request, Book, modeladmin)
    46180
    47         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    48         queryset = changelist.get_query_set()
     181        # Make sure the correct queryset is returned
     182        queryset = changelist.get_query_set(request)
     183        self.assertEqual(list(queryset), [self.django_book])
    49184
    50185        # Make sure the last choice is None and is selected
    51186        filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
     187        self.assertEqual(force_unicode(filterspec.title), u'year')
    53188        choices = list(filterspec.choices(changelist))
    54189        self.assertEqual(choices[-1]['selected'], True)
    55190        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    class FilterSpecsTests(TestCase): 
    59194
    60195        # Make sure the correct choice is selected
    61196        filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
     197        self.assertEqual(force_unicode(filterspec.title), u'year')
    63198        choices = list(filterspec.choices(changelist))
    64199        self.assertEqual(choices[2]['selected'], True)
    65200        self.assertEqual(choices[2]['query_string'], '?year=2002')
    66201
    67     def test_RelatedFilterSpec_ForeignKey(self):
     202    def test_RelatedFieldListFilter_ForeignKey(self):
    68203        modeladmin = BookAdmin(Book, admin.site)
    69204
    70205        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)
     206        changelist = self.get_changelist(request, Book, modeladmin)
    74207
    75         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    76         queryset = changelist.get_query_set()
     208        # Make sure the correct queryset is returned
     209        queryset = changelist.get_query_set(request)
     210        self.assertEqual(list(queryset), [self.gipsy_book])
    77211
    78212        # Make sure the last choice is None and is selected
    79213        filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
     214        self.assertEqual(force_unicode(filterspec.title), u'author')
    81215        choices = list(filterspec.choices(changelist))
    82216        self.assertEqual(choices[-1]['selected'], True)
    83217        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    class FilterSpecsTests(TestCase): 
    87221
    88222        # Make sure the correct choice is selected
    89223        filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     224        self.assertEqual(force_unicode(filterspec.title), u'author')
    91225        # order of choices depends on User model, which has no order
    92226        choice = select_by(filterspec.choices(changelist), "display", "alfred")
    93227        self.assertEqual(choice['selected'], True)
    94228        self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
    95229
    96     def test_RelatedFilterSpec_ManyToMany(self):
     230    def test_RelatedFieldListFilter_ManyToMany(self):
    97231        modeladmin = BookAdmin(Book, admin.site)
    98232
    99233        request = self.request_factory.get('/', {'contributors__isnull': 'True'})
    100234        changelist = self.get_changelist(request, Book, modeladmin)
    101235
    102         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    103         queryset = changelist.get_query_set()
     236        # Make sure the correct queryset is returned
     237        queryset = changelist.get_query_set(request)
     238        self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book])
    104239
    105240        # Make sure the last choice is None and is selected
    106241        filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
     242        self.assertEqual(force_unicode(filterspec.title), u'user')
    108243        choices = list(filterspec.choices(changelist))
    109244        self.assertEqual(choices[-1]['selected'], True)
    110245        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    class FilterSpecsTests(TestCase): 
    114249
    115250        # Make sure the correct choice is selected
    116251        filterspec = changelist.get_filters(request)[0][2]
    117         self.assertEqual(force_unicode(filterspec.title()), u'user')
     252        self.assertEqual(force_unicode(filterspec.title), u'user')
    118253        choice = select_by(filterspec.choices(changelist), "display", "bob")
    119254        self.assertEqual(choice['selected'], True)
    120255        self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
    121256
    122 
    123     def test_RelatedFilterSpec_reverse_relationships(self):
     257    def test_RelatedFieldListFilter_reverse_relationships(self):
    124258        modeladmin = CustomUserAdmin(User, admin.site)
    125259
    126260        # FK relationship -----
    127261        request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
    128262        changelist = self.get_changelist(request, User, modeladmin)
    129263
    130         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    131         queryset = changelist.get_query_set()
     264        # Make sure the correct queryset is returned
     265        queryset = changelist.get_query_set(request)
     266        self.assertEqual(list(queryset), [self.lisa])
    132267
    133268        # Make sure the last choice is None and is selected
    134269        filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
     270        self.assertEqual(force_unicode(filterspec.title), u'book')
    136271        choices = list(filterspec.choices(changelist))
    137272        self.assertEqual(choices[-1]['selected'], True)
    138273        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    class FilterSpecsTests(TestCase): 
    142277
    143278        # Make sure the correct choice is selected
    144279        filterspec = changelist.get_filters(request)[0][0]
    145         self.assertEqual(force_unicode(filterspec.title()), u'book')
     280        self.assertEqual(force_unicode(filterspec.title), u'book')
    146281        choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title)
    147282        self.assertEqual(choice['selected'], True)
    148283        self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
    class FilterSpecsTests(TestCase): 
    151286        request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
    152287        changelist = self.get_changelist(request, User, modeladmin)
    153288
    154         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    155         queryset = changelist.get_query_set()
     289        # Make sure the correct queryset is returned
     290        queryset = changelist.get_query_set(request)
     291        self.assertEqual(list(queryset), [self.alfred])
    156292
    157293        # Make sure the last choice is None and is selected
    158294        filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
     295        self.assertEqual(force_unicode(filterspec.title), u'book')
    160296        choices = list(filterspec.choices(changelist))
    161297        self.assertEqual(choices[-1]['selected'], True)
    162298        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    class FilterSpecsTests(TestCase): 
    166302
    167303        # Make sure the correct choice is selected
    168304        filterspec = changelist.get_filters(request)[0][1]
    169         self.assertEqual(force_unicode(filterspec.title()), u'book')
     305        self.assertEqual(force_unicode(filterspec.title), u'book')
    170306        choice = select_by(filterspec.choices(changelist), "display", self.django_book.title)
    171307        self.assertEqual(choice['selected'], True)
    172308        self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
    173309
    174     def test_BooleanFilterSpec(self):
    175         modeladmin = BoolTestAdmin(BoolTest, admin.site)
     310    def test_BooleanFieldListFilter(self):
     311        modeladmin = BookAdmin(Book, admin.site)
     312        self.verify_BooleanFieldListFilter(modeladmin)
    176313
     314    def test_BooleanFieldListFilter_Tuple(self):
     315        modeladmin = BookAdmin(Book, admin.site)
     316        self.verify_BooleanFieldListFilter(modeladmin)
     317
     318    def verify_BooleanFieldListFilter(self, modeladmin):
    177319        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)
     320        changelist = self.get_changelist(request, Book, modeladmin)
    181321
    182         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
    183         queryset = changelist.get_query_set()
     322        request = self.request_factory.get('/', {'is_best_seller__exact': 0})
     323        changelist = self.get_changelist(request, Book, modeladmin)
    184324
    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')
    188         choices = list(filterspec.choices(changelist))
    189         self.assertEqual(choices[-1]['selected'], False)
    190         self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
     325        # Make sure the correct queryset is returned
     326        queryset = changelist.get_query_set(request)
     327        self.assertEqual(list(queryset), [self.bio_book])
    191328
    192         request = self.request_factory.get('/', {'completed__exact': 1})
    193         changelist = self.get_changelist(request, BoolTest, modeladmin)
     329        # Make sure the correct choice is selected
     330        filterspec = changelist.get_filters(request)[0][3]
     331        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     332        choice = select_by(filterspec.choices(changelist), "display", "No")
     333        self.assertEqual(choice['selected'], True)
     334        self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
     335
     336        request = self.request_factory.get('/', {'is_best_seller__exact': 1})
     337        changelist = self.get_changelist(request, Book, modeladmin)
     338
     339        # Make sure the correct queryset is returned
     340        queryset = changelist.get_query_set(request)
     341        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
    194342
    195343        # Make sure the correct choice is selected
    196         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
     344        filterspec = changelist.get_filters(request)[0][3]
     345        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
    199346        choice = select_by(filterspec.choices(changelist), "display", "Yes")
    200347        self.assertEqual(choice['selected'], True)
    201         self.assertEqual(choice['query_string'], '?completed__exact=1')
     348        self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
    202349
    203 class CustomUserAdmin(UserAdmin):
    204     list_filter = ('books_authored', 'books_contributed')
     350        request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
     351        changelist = self.get_changelist(request, Book, modeladmin)
    205352
    206 class BookAdmin(admin.ModelAdmin):
    207     list_filter = ('year', 'author', 'contributors')
    208     order_by = '-id'
     353        # Make sure the correct queryset is returned
     354        queryset = changelist.get_query_set(request)
     355        self.assertEqual(list(queryset), [self.django_book])
     356
     357        # Make sure the correct choice is selected
     358        filterspec = changelist.get_filters(request)[0][3]
     359        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     360        choice = select_by(filterspec.choices(changelist), "display", "Unknown")
     361        self.assertEqual(choice['selected'], True)
     362        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
     363
     364    def test_SimpleListFilter(self):
     365        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
     366
     367        # Make sure that the first option is 'All' ---------------------------
     368
     369        request = self.request_factory.get('/', {})
     370        changelist = self.get_changelist(request, Book, modeladmin)
     371
     372        # Make sure the correct queryset is returned
     373        queryset = changelist.get_query_set(request)
     374        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
     375
     376        # Make sure the correct choice is selected
     377        filterspec = changelist.get_filters(request)[0][1]
     378        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     379        choices = list(filterspec.choices(changelist))
     380        self.assertEqual(choices[0]['display'], u'All')
     381        self.assertEqual(choices[0]['selected'], True)
     382        self.assertEqual(choices[0]['query_string'], '?')
     383
     384        # Look for books in the 1990s ----------------------------------------
     385
     386        request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
     387        changelist = self.get_changelist(request, Book, modeladmin)
    209388
    210 class BoolTestAdmin(admin.ModelAdmin):
    211     list_filter = ('completed',)
     389        # Make sure the correct queryset is returned
     390        queryset = changelist.get_query_set(request)
     391        self.assertEqual(list(queryset), [self.bio_book])
     392
     393        # Make sure the correct choice is selected
     394        filterspec = changelist.get_filters(request)[0][1]
     395        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     396        choices = list(filterspec.choices(changelist))
     397        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     398        self.assertEqual(choices[1]['selected'], True)
     399        self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
     400
     401        # Look for books in the 2000s ----------------------------------------
     402
     403        request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
     404        changelist = self.get_changelist(request, Book, modeladmin)
     405
     406        # Make sure the correct queryset is returned
     407        queryset = changelist.get_query_set(request)
     408        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     409
     410        # Make sure the correct choice is selected
     411        filterspec = changelist.get_filters(request)[0][1]
     412        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     413        choices = list(filterspec.choices(changelist))
     414        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     415        self.assertEqual(choices[2]['selected'], True)
     416        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
     417
     418        # Combine multiple filters -------------------------------------------
     419
     420        request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
     421        changelist = self.get_changelist(request, Book, modeladmin)
     422
     423        # Make sure the correct queryset is returned
     424        queryset = changelist.get_query_set(request)
     425        self.assertEqual(list(queryset), [self.djangonaut_book])
     426
     427        # Make sure the correct choices are selected
     428        filterspec = changelist.get_filters(request)[0][1]
     429        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     430        choices = list(filterspec.choices(changelist))
     431        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     432        self.assertEqual(choices[2]['selected'], True)
     433        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     434
     435        filterspec = changelist.get_filters(request)[0][0]
     436        self.assertEqual(force_unicode(filterspec.title), u'author')
     437        choice = select_by(filterspec.choices(changelist), "display", "alfred")
     438        self.assertEqual(choice['selected'], True)
     439        self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     440
     441    def test_ListFilter_Without_Title(self):
     442        """
     443        Any filter must define a title.
     444        """
     445        modeladmin = DecadeFilterBookAdminWithoutTitle(Book, admin.site)
     446
     447        request = self.request_factory.get('/', {})
     448       
     449        with self.assertRaises(ImproperlyConfigured) as context_manager:
     450            changelist = self.get_changelist(request, Book, modeladmin)
     451        self.assertEqual(context_manager.exception.message, "The list filter 'DecadeListFilterWithoutTitle' does not specify a 'title'.")
     452
     453    def test_SimpleListFilter_Without_Parameter(self):
     454        """
     455        Any SimpleListFilter must define a parameter_name.
     456        """
     457        modeladmin = DecadeFilterBookAdminWithoutParameter(Book, admin.site)
     458
     459        request = self.request_factory.get('/', {})
     460       
     461        with self.assertRaises(ImproperlyConfigured) as context_manager:
     462            changelist = self.get_changelist(request, Book, modeladmin)
     463        self.assertEqual(context_manager.exception.message, "The list filter 'DecadeListFilterWithoutParameter' does not specify a 'parameter_name'.")
  • tests/regressiontests/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    index 97f6708..6e34ee1 100644
    a b class Gadget(models.Model): 
    611611        return self.name
    612612
    613613class CustomChangeList(ChangeList):
    614     def get_query_set(self):
     614    def get_query_set(self, request):
    615615        return self.root_query_set.filter(pk=9999) # Does not exist
    616616
    617617class GadgetAdmin(admin.ModelAdmin):
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..a1c99b9 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 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