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

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

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..b22a5a2 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
    7 
     7from django.contrib.admin.filterspecs import (ListFilterBase,
     8        SingleQueryParameterListFilter, FieldListFilter, BooleanFieldListFilter,
     9        RelatedFieldListFilter, ChoicesFieldListFilter, DateFieldListFilter,
     10        AllValuesFieldListFilter)
     11       
    812
    913def autodiscover():
    1014    """
  • django/contrib/admin/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    index 965b32b..3d228b9 100644
    a b certain test -- e.g. being a DateField or ForeignKey.  
    77"""
    88
    99from django.db import models
     10from django.core.exceptions import ImproperlyConfigured
    1011from django.utils.encoding import smart_unicode, iri_to_uri
    1112from django.utils.translation import ugettext as _
    1213from django.utils.html import escape
    1314from 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
     15from django.contrib.admin.util import (get_model_from_relation,
     16    reverse_field_path, get_limit_choices_to_from_path)
     17from django.template.defaultfilters import slugify
    1618import datetime
    1719
    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
     20class ListFilterBase(object):
     21    title = None # Human-readable title to appear in the right sidebar.
     22   
     23    def __init__(self, request, params, model, model_admin):
     24        if self.title is None:
     25            raise ImproperlyConfigured("The list filter '%s' does not specify "
     26                                       "a 'title'." % self.__class__.__name__)
    2327        self.params = params
     28        self.request = request
     29       
     30    def has_output(self):
     31        """
     32        Returns True if some choices would be output for the filter.
     33        """
     34        raise NotImplementedError
     35
     36    def get_output_choices(self, cl):
     37        """
     38        Returns choices ready to be output in the template.
     39        """
     40        raise NotImplementedError
     41
     42    def get_query_set(self, queryset):
     43        """
     44        Returns the filtered queryset.
     45        """
     46        raise NotImplementedError
     47   
     48    def used_params(self):
     49        """
     50        Return a list of parameters to consume from the change list
     51        querystring.
     52        """
     53        raise NotImplementedError   
     54
     55
     56
     57class SingleQueryParameterListFilter(ListFilterBase):   
     58    # The parameter that should be used in the query string for that filter.
     59    # Defaults to the title, slugified.
     60    query_parameter = None
     61   
     62    def __init__(self, request, params, model, model_admin):
     63        super(SingleQueryParameterListFilter, self).__init__(request, params, model, model_admin)
     64        if self.query_parameter is None:
     65            self.query_parameter = slugify(self.title)
     66        self.lookup_choices = self.get_choices()
     67   
     68    def has_output(self):
     69        return len(self.lookup_choices) > 0
     70   
     71    def get_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.query_parameter, None)
     77
     78    def get_choices(self):
     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.query_parameter]
     86
     87    def get_output_choices(self, cl):
     88        yield {'selected': self.get_value() is None,
     89               'query_string': cl.get_query_string({}, [self.query_parameter]),
     90               'display': _('All')}
     91        for k, v in self.lookup_choices:
     92            yield {'selected': self.get_value() == k,
     93                   'query_string': cl.get_query_string(
     94                                   {self.query_parameter: k},
     95                                   []),
     96                   'display': v}
     97
     98
     99           
     100class FieldListFilter(ListFilterBase):
     101    _field_list_filters = []
     102    _take_priority_index = 0
     103   
     104    def __init__(self, field, request, params, model, model_admin, \
     105                 field_path):
     106        self.field = field
    24107        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)
     108        self.title = field_path
     109        super(FieldListFilter, self).__init__(request, params, model, \
     110                                              model_admin)
    41111
    42112    def has_output(self):
    43113        return True
     114       
     115    def get_query_set(self, queryset):
     116        for p in self.used_params():
     117            if p in self.params:
     118                return queryset.filter(**{p: self.params[p]})
     119       
     120    @classmethod
     121    def register(cls, test, list_filter_class, take_priority=False):
     122        if take_priority:
     123            # This is to allow overriding the default filters for certain types
     124            # of fields with some custom filters. The first found in the list
     125            # is used in priority.
     126            cls._field_list_filters.insert(cls._take_priority_index, (test, list_filter_class))
     127            _take_priority_index += 1
     128        else:
     129            cls._field_list_filters.append((test, list_filter_class))
     130   
     131    @classmethod
     132    def create(cls, field, request, params, model, model_admin, field_path):
     133        for test, list_filter_class in cls._field_list_filters:
     134            if test(field):
     135                return list_filter_class(field, request, params, model, model_admin,
     136                               field_path=field_path)
    44137
    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))
     138       
     139class RelatedFieldListFilter(FieldListFilter):
     140    def __init__(self, field, request, params, model, model_admin,
     141                 field_path):
     142        super(RelatedFieldListFilter, self).__init__(
     143            field, request, params, model, model_admin, field_path)
    63144
    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,
     145        other_model = get_model_from_relation(field)
     146        if isinstance(field, (models.ManyToManyField,
    72147                          models.related.RelatedObject)):
    73148            # no direct field on this model, get name from other model
    74149            self.lookup_title = other_model._meta.verbose_name
    75150        else:
    76             self.lookup_title = f.verbose_name # use field name
     151            self.lookup_title = field.verbose_name # use field name
    77152        rel_name = other_model._meta.pk.name
    78153        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
    79154        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
    80155        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    81156        self.lookup_val_isnull = request.GET.get(
    82157                                      self.lookup_kwarg_isnull, None)
    83         self.lookup_choices = f.get_choices(include_blank=False)
     158        self.lookup_choices = field.get_choices(include_blank=False)
     159        self.title = self.lookup_title
    84160
    85161    def has_output(self):
    86162        if isinstance(self.field, models.related.RelatedObject) \
    class RelatedFilterSpec(FilterSpec):  
    91167            extra = 0
    92168        return len(self.lookup_choices) + extra > 1
    93169
    94     def title(self):
    95         return self.lookup_title
    96 
    97     def choices(self, cl):
     170    def used_params(self):
     171        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
     172   
     173    def get_output_choices(self, cl):
    98174        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    99175        yield {'selected': self.lookup_val is None
    100176                           and not self.lookup_val_isnull,
    class RelatedFilterSpec(FilterSpec):  
    117193                                   [self.lookup_kwarg]),
    118194                   'display': EMPTY_CHANGELIST_VALUE}
    119195
    120 FilterSpec.register(lambda f: (
     196FieldListFilter.register(lambda f: (
    121197        hasattr(f, 'rel') and bool(f.rel) or
    122         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     198        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
    123199
    124 class BooleanFieldFilterSpec(FilterSpec):
     200class BooleanFieldListFilter(FieldListFilter):
    125201    def __init__(self, f, request, params, model, model_admin,
    126                  field_path=None):
    127         super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
     202                 field_path):
     203        super(BooleanFieldListFilter, self).__init__(f, request, params, model,
    128204                                                     model_admin,
    129                                                      field_path=field_path)
     205                                                     field_path)
    130206        self.lookup_kwarg = '%s__exact' % self.field_path
    131207        self.lookup_kwarg2 = '%s__isnull' % self.field_path
    132208        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133209        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134210
    135     def title(self):
    136         return self.field.verbose_name
    137 
    138     def choices(self, cl):
     211    def used_params(self):
     212        return [self.lookup_kwarg, self.lookup_kwarg2]
     213   
     214    def get_output_choices(self, cl):
    139215        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    140216            yield {'selected': self.lookup_val == v and not self.lookup_val2,
    141217                   'query_string': cl.get_query_string(
    class BooleanFieldFilterSpec(FilterSpec):  
    149225                                   [self.lookup_kwarg]),
    150226                   'display': _('Unknown')}
    151227
    152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
     228FieldListFilter.register(lambda f: isinstance(f, models.BooleanField)
    153229                              or isinstance(f, models.NullBooleanField),
    154                                  BooleanFieldFilterSpec)
     230                                 BooleanFieldListFilter)
    155231
    156 class ChoicesFilterSpec(FilterSpec):
     232class ChoicesFieldListFilter(FieldListFilter):
    157233    def __init__(self, f, request, params, model, model_admin,
    158                  field_path=None):
    159         super(ChoicesFilterSpec, self).__init__(f, request, params, model,
     234                 field_path):
     235        super(ChoicesFieldListFilter, self).__init__(f, request, params, model,
    160236                                                model_admin,
    161                                                 field_path=field_path)
     237                                                field_path)
    162238        self.lookup_kwarg = '%s__exact' % self.field_path
    163         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     239        self.lookup_val = request.GET.get(self.lookup_kwarg)
    164240
    165     def choices(self, cl):
     241    def used_params(self):
     242        return [self.lookup_kwarg]
     243       
     244    def get_output_choices(self, cl):
    166245        yield {'selected': self.lookup_val is None,
    167246               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
    168247               'display': _('All')}
    class ChoicesFilterSpec(FilterSpec):  
    172251                                    {self.lookup_kwarg: k}),
    173252                    'display': v}
    174253
    175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     254FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
    176255
    177 class DateFieldFilterSpec(FilterSpec):
     256class DateFieldListFilter(FieldListFilter):
    178257    def __init__(self, f, request, params, model, model_admin,
    179                  field_path=None):
    180         super(DateFieldFilterSpec, self).__init__(f, request, params, model,
     258                 field_path):
     259        super(DateFieldListFilter, self).__init__(f, request, params, model,
    181260                                                  model_admin,
    182                                                   field_path=field_path)
     261                                                  field_path)
    183262
    184263        self.field_generic = '%s__' % self.field_path
    185264
    class DateFieldFilterSpec(FilterSpec):  
    192271                    and today.strftime('%Y-%m-%d 23:59:59') \
    193272                    or today.strftime('%Y-%m-%d')
    194273
     274        self.lookup_kwarg_year = '%s__year' % self.field_path
     275        self.lookup_kwarg_month = '%s__month' % self.field_path
     276        self.lookup_kwarg_day = '%s__day' % self.field_path
     277        self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
     278        self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
     279       
    195280        self.links = (
    196281            (_('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:
     282            (_('Today'), {self.lookup_kwarg_year: str(today.year),
     283                          self.lookup_kwarg_month: str(today.month),
     284                          self.lookup_kwarg_day: str(today.day)}),
     285            (_('Past 7 days'), {self.lookup_kwarg_past_7_days_gte:
    201286                                    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)})
     287                                self.lookup_kwarg_past_7_days_lte:
     288                                    today_str}),
     289            (_('This month'), {self.lookup_kwarg_year: str(today.year),
     290                               self.lookup_kwarg_month: str(today.month)}),
     291            (_('This year'), {self.lookup_kwarg_year: str(today.year)})
    206292        )
    207293
    208     def title(self):
    209         return self.field.verbose_name
    210 
    211     def choices(self, cl):
     294    def used_params(self):
     295        return [self.lookup_kwarg_year, self.lookup_kwarg_month,
     296                self.lookup_kwarg_day, self.lookup_kwarg_past_7_days_gte,
     297                self.lookup_kwarg_past_7_days_lte]
     298   
     299    def get_query_set(self, queryset):
     300        """
     301        Override the default behaviour since there can be multiple query
     302        string parameters used for the same date filter (e.g. year + month).
     303        """
     304        query_dict = {}
     305        for p in self.used_params():
     306            if p in self.params:
     307                query_dict[p] = self.params[p]
     308        if len(query_dict):
     309            return queryset.filter(**query_dict)
     310   
     311    def get_output_choices(self, cl):
    212312        for title, param_dict in self.links:
    213313            yield {'selected': self.date_params == param_dict,
    214314                   'query_string': cl.get_query_string(
    class DateFieldFilterSpec(FilterSpec):  
    216316                                   [self.field_generic]),
    217317                   'display': title}
    218318
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     319FieldListFilter.register(lambda f: isinstance(f, models.DateField),
     320                              DateFieldListFilter)
    221321
    222322
    223323# 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):
     324# if a field is eligible to use the BooleanFieldListFilter, that'd be much
     325# more appropriate, and the AllValuesFieldListFilter won't get used for it.
     326class AllValuesFieldListFilter(FieldListFilter):
    227327    def __init__(self, f, request, params, model, model_admin,
    228                  field_path=None):
    229         super(AllValuesFilterSpec, self).__init__(f, request, params, model,
     328                 field_path):
     329        super(AllValuesFieldListFilter, self).__init__(f, request, params, model,
    230330                                                  model_admin,
    231                                                   field_path=field_path)
     331                                                  field_path)
    232332        self.lookup_kwarg = self.field_path
    233333        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
    234334        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    class AllValuesFilterSpec(FilterSpec):  
    245345        self.lookup_choices = \
    246346            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
    247347
    248     def title(self):
    249         return self.field.verbose_name
    250 
    251     def choices(self, cl):
     348    def used_params(self):
     349        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
     350       
     351    def get_output_choices(self, cl):
    252352        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    253353        yield {'selected': self.lookup_val is None
    254354                           and self.lookup_val_isnull is None,
    class AllValuesFilterSpec(FilterSpec):  
    276376                                    [self.lookup_kwarg]),
    277377                    'display': EMPTY_CHANGELIST_VALUE}
    278378
    279 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     379FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  • django/contrib/admin/templatetags/admin_list.py

    diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
    index fdf082b..3125116 100644
    a b def search_form(cl):  
    317317search_form = register.inclusion_tag('admin/search_form.html')(search_form)
    318318
    319319def admin_list_filter(cl, spec):
    320     return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
     320    return {'title': spec.title, 'choices' : list(spec.get_output_choices(cl))}
    321321admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
    322322
    323323def admin_actions(context):
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 159afa4..b788a1a 100644
    a b from django.db import models  
    33from django.db.models.fields import FieldDoesNotExist
    44from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
    55    _get_foreign_key)
     6from django.contrib.admin.filterspecs import ListFilterBase, FieldListFilter
    67from django.contrib.admin.util import get_fields_from_path, NotRelationField
    78from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
    89    HORIZONTAL, VERTICAL)
    def validate(cls, model):  
    5455    # list_filter
    5556    if hasattr(cls, 'list_filter'):
    5657        check_isseq(cls, 'list_filter', cls.list_filter)
    57         for idx, fpath in enumerate(cls.list_filter):
    58             try:
    59                 get_fields_from_path(model, fpath)
    60             except (NotRelationField, FieldDoesNotExist), e:
    61                 raise ImproperlyConfigured(
    62                     "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
    63                         cls.__name__, idx, fpath
    64                     )
    65                 )
     58        for idx, item in enumerate(cls.list_filter):
     59            # There are three options for specifying a filter:
     60            #   1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
     61            #   2: ('field', SomeFieldListFilter) - a field-based list filter class
     62            #   3: SomeListFilter - a non-field list filter class
     63            if callable(item) and not isinstance(item, models.Field):
     64                # If item is option 3, it should be a ListFilterBase...
     65                if not issubclass(item, ListFilterBase):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not a descendant of ListFilterBase."
     68                            % (cls.__name__, idx, item.__name__))
     69                # ...  but not a FieldListFilter.
     70                if issubclass(item, FieldListFilter):
     71                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     72                            " which is of type FieldListFilter but is not"
     73                            " associated with a field name."
     74                            % (cls.__name__, idx, item.__name__))
     75            else:
     76                try:
     77                    # Check for option #2 (tuple)
     78                    field, list_filter_class = item
     79                except (TypeError, ValueError):
     80                    # item is option #1
     81                    field = item
     82                else:
     83                    # item is option #2
     84                    if not issubclass(list_filter_class, FieldListFilter):
     85                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
     86                            " is '%s' which is not of type FieldListFilter."
     87                            % (cls.__name__, idx, list_filter_class.__name__))
     88                # Validate the field string
     89                try:
     90                    get_fields_from_path(model, field)
     91                except (NotRelationField, FieldDoesNotExist), e:
     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..7e2165f 100644
    a b  
    1 from django.contrib.admin.filterspecs import FilterSpec
     1from django.contrib.admin.filterspecs import SingleQueryParameterListFilter, FieldListFilter
    22from django.contrib.admin.options import IncorrectLookupParameters
    33from django.contrib.admin.util import quote, get_fields_from_path
    44from django.core.exceptions import SuspiciousOperation
    def field_needs_distinct(field):  
    3737
    3838class ChangeList(object):
    3939    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):
     40        self.request = request
    4041        self.model = model
    4142        self.opts = model._meta
    4243        self.lookup_opts = self.opts
    class ChangeList(object):  
    7172        self.order_field, self.order_type = self.get_ordering()
    7273        self.query = request.GET.get(SEARCH_VAR, '')
    7374        self.query_set = self.get_query_set()
    74         self.get_results(request)
     75        self.get_results()
    7576        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)
    7777        self.pk_attname = self.lookup_opts.pk.attname
    7878
    79     def get_filters(self, request):
     79    def get_filters(self):
    8080        filter_specs = []
    8181        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)
     82            for item in self.list_filter:
     83                if callable(item):
     84                    # This is simply a custom list filter class.
     85                    spec = item(self.request, self.cleaned_params, self.model, self.model_admin)
     86                else:
     87                    field_path = None
     88                    try:
     89                        # This is custom FieldListFilter class for a given field.
     90                        field, field_list_filter_class = item
     91                    except (TypeError, ValueError):
     92                        # This is simply a field name, so use the default
     93                        # FieldListFilter class that has been registered for
     94                        # the type of the given field.
     95                        field, field_list_filter_class = item, FieldListFilter.create
     96                    if not isinstance(field, models.Field):
     97                        field_path = field
     98                        field = get_fields_from_path(self.model, field_path)[-1]
     99                    spec = field_list_filter_class(field, self.request, self.cleaned_params, self.model,
     100                            self.model_admin, field_path=field_path)
    87101                if spec and spec.has_output():
    88102                    filter_specs.append(spec)
    89103        return filter_specs, bool(filter_specs)
    class ChangeList(object):  
    104118                p[k] = v
    105119        return '?%s' % urlencode(p)
    106120
    107     def get_results(self, request):
    108         paginator = self.model_admin.get_paginator(request, self.query_set, self.list_per_page)
     121    def get_results(self):
     122        paginator = self.model_admin.get_paginator(self.request, self.query_set, self.list_per_page)
    109123        # Get the number of objects, with admin filters applied.
    110124        result_count = paginator.count
    111125
    class ChangeList(object):  
    174188        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
    175189            order_type = params[ORDER_TYPE_VAR]
    176190        return order_field, order_type
    177 
     191   
     192    def apply_list_filters(self, qs, lookup_params):
     193        for filter_spec in self.filter_specs:
     194            new_qs = filter_spec.get_query_set(qs)
     195            if new_qs is not None:
     196                qs = new_qs
     197                for param in filter_spec.used_params():
     198                    try:
     199                        del lookup_params[param]
     200                    except KeyError:
     201                        pass
     202        return qs
     203   
    178204    def get_query_set(self):
    179205        use_distinct = False
    180206
    class ChangeList(object):  
    196222                field_name = key.split('__', 1)[0]
    197223                try:
    198224                    f = self.lookup_opts.get_field_by_name(field_name)[0]
     225                    use_distinct = field_needs_distinct(f)
    199226                except models.FieldDoesNotExist:
    200                     raise IncorrectLookupParameters
    201                 use_distinct = field_needs_distinct(f)
    202 
     227                    # It might be a custom NonFieldFilter
     228                    pass
     229               
    203230            # if key ends with __in, split parameter into separate values
    204231            if key.endswith('__in'):
    205232                value = value.split(',')
    class ChangeList(object):  
    217244                raise SuspiciousOperation(
    218245                    "Filtering by %s not allowed" % key
    219246                )
    220 
    221         # Apply lookup parameters from the query string.
     247       
     248        # Keep a copy of cleaned querystring parameters so they can be passed
     249        # to the list filters.
     250        self.cleaned_params = lookup_params.copy()
     251       
     252        self.filter_specs, self.has_filters = self.get_filters()
     253       
     254        # Let every list filter modify the qs and params to its liking
     255        qs = self.apply_list_filters(qs, lookup_params)
     256       
     257        # Apply the remaining lookup parameters from the query string (i.e.
     258        # those that haven't already been processed by the filters).
    222259        try:
    223260            qs = qs.filter(**lookup_params)
    224261        # Naked except! Because we don't have any other way of validating "params".
  • 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..b1cffe1 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::
    535 
    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:
     528    .. versionchanged:: 1.4
    541529
     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:
     532   
    542533        .. image:: _images/users_changelist.png
    543 
    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')
     534   
     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.SingleQueryParameterListFilter`,
     554          where you need to provide a few attributes and override a few
     555          methods::
     556         
     557              from django.contrib.admin import SingleQueryParameterListFilter
     558              from django.db.models import Q
     559           
     560              class DecadeBornListFilter(SingleQueryParameterListFilter):
     561                  # Human-readable title which will be displayed in the
     562                  # right sidebar just above the filter options.
     563                  title = u'decade born'
     564               
     565                  # Parameter for the filter that will be used in the url query.
     566                  # Providing this attribute is optional. If it is not provided then a
     567                  # slugified version of the title will automatically be used instead
     568                  # (that is, 'decade-born' in this example).
     569                  query_parameter = u'decade'
     570               
     571                  def get_choices(self):
     572                      """
     573                      Returns a list of tuples. The first element in each tuple
     574                      is the coded value for the option that will appear in the
     575                      url query. The second element is the human-readable name
     576                      for the option that will appear in the right sidebar.
     577                      """
     578                      return (
     579                          (u'80s', u'in the eighties'),
     580                          (u'other', u'other'),
     581                      )
     582               
     583                  def get_query_set(self, queryset):
     584                      """
     585                      Returns the filtered queryset based on the value provided
     586                      in the query string and retrievable via `get_value()`.
     587                      """
     588                      # First, retrieve the requested value (either '80s' or 'other').
     589                      decade = self.get_value()
     590                      # Then decide how to filter the queryset based on that value.
     591                      if decade == u'80s':
     592                          return queryset.filter(birthday__year__gte=1980,
     593                                                 birthday__year__lte=1989)
     594                      if decade == u'other':
     595                          return queryset.filter(Q(year__lte=1979) |
     596                                                 Q(year__gte=1990))
     597   
     598              class PersonAdmin(ModelAdmin):
     599                  list_filter = (DecadeBornListFilter,)
     600       
     601          .. note::
     602         
     603              As a convenience, the ``HttpRequest`` object is accessible using
     604              ``self.request`` from any of the filter's methods, for example::
     605             
     606                  class DecadeBornListFilter(SingleQueryParameterListFilter):
     607                 
     608                      def get_choices(self):
     609                          if self.request.user.is_authenticated():
     610                              return (
     611                                  (u'80s', u'in the eighties'),
     612                                  (u'other', u'other'),
     613                              )
     614                          else:
     615                              return (
     616                                  (u'90s', u'in the nineties'),
     617                              )
     618       
     619        * a tuple, where the first element is a field name and the second
     620          element is a class inheriting from
     621          :mod:`django.contrib.admin.FieldListFilter`, for example::
     622         
     623              from django.contrib.admin import BooleanFieldListFilter
     624         
     625              class PersonAdmin(ModelAdmin):
     626                  list_filter = (('is_staff', BooleanFieldListFilter),)
     627         
     628          .. note::
     629         
     630              The ``FieldListFilter`` API is currently considered internal and
     631              prone to refactoring.
    552632
    553633.. attribute:: ModelAdmin.list_per_page
    554634
  • tests/regressiontests/admin_changelist/tests.py

    diff --git a/tests/regressiontests/admin_changelist/tests.py b/tests/regressiontests/admin_changelist/tests.py
    index 5186508..ae0db75 100644
    a b class ChangeListTests(TransactionTestCase):  
    133133                m.list_filter, m.date_hierarchy, m.search_fields,
    134134                m.list_select_related, m.list_per_page, m.list_editable, m)
    135135
    136         cl.get_results(request)
     136        cl.get_results()
    137137        self.assertIsInstance(cl.paginator, CustomPaginator)
    138138
    139139    def test_distinct_for_m2m_in_list_filter(self):
    class ChangeListTests(TransactionTestCase):  
    155155                m.search_fields, m.list_select_related, m.list_per_page,
    156156                m.list_editable, m)
    157157
    158         cl.get_results(request)
     158        cl.get_results()
    159159
    160160        # There's only one Group instance
    161161        self.assertEqual(cl.result_count, 1)
    class ChangeListTests(TransactionTestCase):  
    178178                m.search_fields, m.list_select_related, m.list_per_page,
    179179                m.list_editable, m)
    180180
    181         cl.get_results(request)
     181        cl.get_results()
    182182
    183183        # There's only one Group instance
    184184        self.assertEqual(cl.result_count, 1)
    class ChangeListTests(TransactionTestCase):  
    202202                m.search_fields, m.list_select_related, m.list_per_page,
    203203                m.list_editable, m)
    204204
    205         cl.get_results(request)
     205        cl.get_results()
    206206
    207207        # There's only one Quartet instance
    208208        self.assertEqual(cl.result_count, 1)
    class ChangeListTests(TransactionTestCase):  
    226226                m.search_fields, m.list_select_related, m.list_per_page,
    227227                m.list_editable, m)
    228228
    229         cl.get_results(request)
     229        cl.get_results()
    230230
    231231        # There's only one ChordsBand instance
    232232        self.assertEqual(cl.result_count, 1)
  • tests/regressiontests/admin_filterspecs/models.py

    diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
    index 5b284c7..5e81d1c 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 
     9    is_best_seller = models.NullBooleanField(default=0)
     10    date_registered = models.DateField(null=True)
     11   
    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..cef5b98 100644
    a b  
     1import datetime
     2import calendar
     3
    14from django.contrib.auth.admin import UserAdmin
    25from django.test import TestCase
    36from django.test.client import RequestFactory
    from django.contrib.auth.models import User  
    58from django.contrib import admin
    69from django.contrib.admin.views.main import ChangeList
    710from django.utils.encoding import force_unicode
     11from django.contrib.admin.filterspecs import (SingleQueryParameterListFilter,
     12    BooleanFieldListFilter, FieldListFilter)
    813
    9 from models import Book, BoolTest
     14from models import Book
    1015
    1116def select_by(dictlist, key, value):
    1217    return [x for x in dictlist if x[key] == value][0]
    1318
    14 class FilterSpecsTests(TestCase):
     19
     20
     21class DecadeListFilterBase(SingleQueryParameterListFilter):
     22   
     23    def get_choices(self):
     24        return (
     25            (u'the 90s', u'the 1990\'s'),
     26            (u'the 00s', u'the 2000\'s'),
     27            (u'other', u'other decades'),
     28        )
     29   
     30    def get_query_set(self, queryset):
     31        decade = self.get_value()
     32        if decade == u'the 90s':
     33            return queryset.filter(year__gte=1990, year__lte=1999)
     34        if decade == u'the 00s':
     35            return queryset.filter(year__gte=2000, year__lte=2009)
     36
     37class DecadeListFilterWithTitle(DecadeListFilterBase):
     38    title = u'publication decade'
     39   
     40class DecadeListFilterWithParamName(DecadeListFilterBase):
     41    title = u'another publication decade'
     42    query_parameter = u'blah'
     43
     44class ListFiltersTests(TestCase):
    1545
    1646    def setUp(self):
     47        self.today = datetime.date.today()
     48        self.one_week_ago = self.today - datetime.timedelta(days=7)
     49       
     50        self.request_factory = RequestFactory()
     51       
    1752        # Users
    1853        self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
    1954        self.bob = User.objects.create_user('bob', 'bob@example.com')
    20         lisa = User.objects.create_user('lisa', 'lisa@example.com')
    21 
    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()
    28 
    29         # BoolTests
    30         self.trueTest = BoolTest.objects.create(completed=True)
    31         self.falseTest = BoolTest.objects.create(completed=False)
    32 
    33         self.request_factory = RequestFactory()
     55        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
    3456
     57        # Books
     58        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)
     59        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False)
     60        self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today)
     61        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago)
     62        self.gipsy_book.contributors = [self.bob, self.lisa]
     63        self.gipsy_book.save()       
    3564
    3665    def get_changelist(self, request, model, modeladmin):
    3766        return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
    3867            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    3968            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
    4069
    41     def test_AllValuesFilterSpec(self):
     70    def test_DateFieldListFilter(self):
     71        modeladmin = BookAdmin(Book, admin.site)
     72       
     73        request = self.request_factory.get('/')
     74        changelist = self.get_changelist(request, Book, modeladmin)
     75
     76        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     77                                                 'date_registered__month': self.today.month,
     78                                                 'date_registered__day': self.today.day})
     79        changelist = self.get_changelist(request, Book, modeladmin)
     80       
     81        # Make sure the correct queryset is returned
     82        queryset = changelist.get_query_set()
     83        self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     84       
     85        # Make sure the correct choice is selected
     86        filterspec = changelist.get_filters()[0][4]
     87        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     88        choice = select_by(filterspec.get_output_choices(changelist), "display", "Today")
     89        self.assertEqual(choice['selected'], True)
     90        self.assertEqual(choice['query_string'], '?date_registered__day=%s'
     91                                                 '&date_registered__month=%s'
     92                                                 '&date_registered__year=%s'
     93                                                % (self.today.day, self.today.month, self.today.year))
     94       
     95        request = self.request_factory.get('/', {'date_registered__year': self.today.year,
     96                                                 'date_registered__month': self.today.month})
     97        changelist = self.get_changelist(request, Book, modeladmin)
     98       
     99        # Make sure the correct queryset is returned
     100        queryset = changelist.get_query_set()
     101        if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month):
     102            # In case one week ago is in the same month.
     103            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     104        else:
     105            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     106       
     107        # Make sure the correct choice is selected
     108        filterspec = changelist.get_filters()[0][4]
     109        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     110        choice = select_by(filterspec.get_output_choices(changelist), "display", "This month")
     111        self.assertEqual(choice['selected'], True)
     112        self.assertEqual(choice['query_string'], '?date_registered__month=%s'
     113                                                 '&date_registered__year=%s'
     114                                                % (self.today.month, self.today.year))
     115   
     116        request = self.request_factory.get('/', {'date_registered__year': self.today.year})
     117        changelist = self.get_changelist(request, Book, modeladmin)
     118       
     119        # Make sure the correct queryset is returned
     120        queryset = changelist.get_query_set()
     121        if self.today.year == self.one_week_ago.year:
     122            # In case one week ago is in the same year.
     123            self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     124        else:
     125            self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
     126       
     127        # Make sure the correct choice is selected
     128        filterspec = changelist.get_filters()[0][4]
     129        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     130        choice = select_by(filterspec.get_output_choices(changelist), "display", "This year")
     131        self.assertEqual(choice['selected'], True)
     132        self.assertEqual(choice['query_string'], '?date_registered__year=%s'
     133                                                % (self.today.year))
     134
     135        request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'),
     136                                                 'date_registered__lte': self.today.strftime('%Y-%m-%d')})
     137        changelist = self.get_changelist(request, Book, modeladmin)
     138       
     139        # Make sure the correct queryset is returned
     140        queryset = changelist.get_query_set()
     141        self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
     142       
     143        # Make sure the correct choice is selected
     144        filterspec = changelist.get_filters()[0][4]
     145        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     146        choice = select_by(filterspec.get_output_choices(changelist), "display", "Past 7 days")
     147        self.assertEqual(choice['selected'], True)
     148        self.assertEqual(choice['query_string'], '?date_registered__gte=%s'
     149                                                 '&date_registered__lte=%s'
     150                                                % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d')))
     151       
     152    def test_AllValuesFieldListFilter(self):
    42153        modeladmin = BookAdmin(Book, admin.site)
    43154
    44155        request = self.request_factory.get('/', {'year__isnull': 'True'})
    45156        changelist = self.get_changelist(request, Book, modeladmin)
    46157
    47         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     158        # Make sure the correct queryset is returned
    48159        queryset = changelist.get_query_set()
     160        self.assertEqual(list(queryset), [self.django_book])
    49161
    50162        # Make sure the last choice is None and is selected
    51         filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
    53         choices = list(filterspec.choices(changelist))
     163        filterspec = changelist.get_filters()[0][0]
     164        self.assertEqual(force_unicode(filterspec.title), u'year')
     165        choices = list(filterspec.get_output_choices(changelist))
    54166        self.assertEqual(choices[-1]['selected'], True)
    55167        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    56168
    class FilterSpecsTests(TestCase):  
    58170        changelist = self.get_changelist(request, Book, modeladmin)
    59171
    60172        # Make sure the correct choice is selected
    61         filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
    63         choices = list(filterspec.choices(changelist))
     173        filterspec = changelist.get_filters()[0][0]
     174        self.assertEqual(force_unicode(filterspec.title), u'year')
     175        choices = list(filterspec.get_output_choices(changelist))
    64176        self.assertEqual(choices[2]['selected'], True)
    65177        self.assertEqual(choices[2]['query_string'], '?year=2002')
    66178
    67     def test_RelatedFilterSpec_ForeignKey(self):
     179    def test_RelatedFieldListFilter_ForeignKey(self):
    68180        modeladmin = BookAdmin(Book, admin.site)
    69181
    70182        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)
     183        changelist = self.get_changelist(request, Book, modeladmin)
    74184
    75         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     185        # Make sure the correct queryset is returned
    76186        queryset = changelist.get_query_set()
     187        self.assertEqual(list(queryset), [self.gipsy_book])
    77188
    78189        # Make sure the last choice is None and is selected
    79         filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
    81         choices = list(filterspec.choices(changelist))
     190        filterspec = changelist.get_filters()[0][1]
     191        self.assertEqual(force_unicode(filterspec.title), u'author')
     192        choices = list(filterspec.get_output_choices(changelist))
    82193        self.assertEqual(choices[-1]['selected'], True)
    83194        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    84195
    class FilterSpecsTests(TestCase):  
    86197        changelist = self.get_changelist(request, Book, modeladmin)
    87198
    88199        # Make sure the correct choice is selected
    89         filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     200        filterspec = changelist.get_filters()[0][1]
     201        self.assertEqual(force_unicode(filterspec.title), u'author')
    91202        # order of choices depends on User model, which has no order
    92         choice = select_by(filterspec.choices(changelist), "display", "alfred")
     203        choice = select_by(filterspec.get_output_choices(changelist), "display", "alfred")
    93204        self.assertEqual(choice['selected'], True)
    94205        self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
    95206
    96     def test_RelatedFilterSpec_ManyToMany(self):
     207    def test_RelatedFieldListFilter_ManyToMany(self):
    97208        modeladmin = BookAdmin(Book, admin.site)
    98209
    99210        request = self.request_factory.get('/', {'contributors__isnull': 'True'})
    100211        changelist = self.get_changelist(request, Book, modeladmin)
    101212
    102         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     213        # Make sure the correct queryset is returned
    103214        queryset = changelist.get_query_set()
     215        self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book])
    104216
    105217        # Make sure the last choice is None and is selected
    106         filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
    108         choices = list(filterspec.choices(changelist))
     218        filterspec = changelist.get_filters()[0][2]
     219        self.assertEqual(force_unicode(filterspec.title), u'user')
     220        choices = list(filterspec.get_output_choices(changelist))
    109221        self.assertEqual(choices[-1]['selected'], True)
    110222        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    111223
    class FilterSpecsTests(TestCase):  
    113225        changelist = self.get_changelist(request, Book, modeladmin)
    114226
    115227        # Make sure the correct choice is selected
    116         filterspec = changelist.get_filters(request)[0][2]
    117         self.assertEqual(force_unicode(filterspec.title()), u'user')
    118         choice = select_by(filterspec.choices(changelist), "display", "bob")
     228        filterspec = changelist.get_filters()[0][2]
     229        self.assertEqual(force_unicode(filterspec.title), u'user')
     230        choice = select_by(filterspec.get_output_choices(changelist), "display", "bob")
    119231        self.assertEqual(choice['selected'], True)
    120232        self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
    121233
    122 
    123     def test_RelatedFilterSpec_reverse_relationships(self):
     234    def test_RelatedFieldListFilter_reverse_relationships(self):
    124235        modeladmin = CustomUserAdmin(User, admin.site)
    125236
    126237        # FK relationship -----
    127238        request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
    128239        changelist = self.get_changelist(request, User, modeladmin)
    129240
    130         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     241        # Make sure the correct queryset is returned
    131242        queryset = changelist.get_query_set()
     243        self.assertEqual(list(queryset), [self.lisa])
    132244
    133245        # Make sure the last choice is None and is selected
    134         filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
    136         choices = list(filterspec.choices(changelist))
     246        filterspec = changelist.get_filters()[0][0]
     247        self.assertEqual(force_unicode(filterspec.title), u'book')
     248        choices = list(filterspec.get_output_choices(changelist))
    137249        self.assertEqual(choices[-1]['selected'], True)
    138250        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    139251
    class FilterSpecsTests(TestCase):  
    141253        changelist = self.get_changelist(request, User, modeladmin)
    142254
    143255        # Make sure the correct choice is selected
    144         filterspec = changelist.get_filters(request)[0][0]
    145         self.assertEqual(force_unicode(filterspec.title()), u'book')
    146         choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title)
     256        filterspec = changelist.get_filters()[0][0]
     257        self.assertEqual(force_unicode(filterspec.title), u'book')
     258        choice = select_by(filterspec.get_output_choices(changelist), "display", self.bio_book.title)
    147259        self.assertEqual(choice['selected'], True)
    148260        self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
    149261
    class FilterSpecsTests(TestCase):  
    151263        request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
    152264        changelist = self.get_changelist(request, User, modeladmin)
    153265
    154         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     266        # Make sure the correct queryset is returned
    155267        queryset = changelist.get_query_set()
     268        self.assertEqual(list(queryset), [self.alfred])
    156269
    157270        # Make sure the last choice is None and is selected
    158         filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
    160         choices = list(filterspec.choices(changelist))
     271        filterspec = changelist.get_filters()[0][1]
     272        self.assertEqual(force_unicode(filterspec.title), u'book')
     273        choices = list(filterspec.get_output_choices(changelist))
    161274        self.assertEqual(choices[-1]['selected'], True)
    162275        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    163276
    class FilterSpecsTests(TestCase):  
    165278        changelist = self.get_changelist(request, User, modeladmin)
    166279
    167280        # Make sure the correct choice is selected
    168         filterspec = changelist.get_filters(request)[0][1]
    169         self.assertEqual(force_unicode(filterspec.title()), u'book')
    170         choice = select_by(filterspec.choices(changelist), "display", self.django_book.title)
     281        filterspec = changelist.get_filters()[0][1]
     282        self.assertEqual(force_unicode(filterspec.title), u'book')
     283        choice = select_by(filterspec.get_output_choices(changelist), "display", self.django_book.title)
    171284        self.assertEqual(choice['selected'], True)
    172285        self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
    173286
    174     def test_BooleanFilterSpec(self):
    175         modeladmin = BoolTestAdmin(BoolTest, admin.site)
    176 
     287    def test_BooleanFieldListFilter(self):
     288        modeladmin = BookAdmin(Book, admin.site)
     289        self.verify_BooleanFieldListFilter(modeladmin)
     290       
     291    def test_BooleanFieldListFilter_Tuple(self):
     292        modeladmin = BookAdmin(Book, admin.site)
     293        self.verify_BooleanFieldListFilter(modeladmin)
     294       
     295    def verify_BooleanFieldListFilter(self, modeladmin):
    177296        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)
     297        changelist = self.get_changelist(request, Book, modeladmin)
    181298
    182         # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
     299        request = self.request_factory.get('/', {'is_best_seller__exact': 0})
     300        changelist = self.get_changelist(request, Book, modeladmin)
     301       
     302        # Make sure the correct queryset is returned
    183303        queryset = changelist.get_query_set()
     304        self.assertEqual(list(queryset), [self.bio_book])
    184305
    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')
     306        # Make sure the correct choice is selected
     307        filterspec = changelist.get_filters()[0][3]
     308        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     309        choice = select_by(filterspec.get_output_choices(changelist), "display", "No")
     310        self.assertEqual(choice['selected'], True)
     311        self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
    191312
    192         request = self.request_factory.get('/', {'completed__exact': 1})
    193         changelist = self.get_changelist(request, BoolTest, modeladmin)
     313        request = self.request_factory.get('/', {'is_best_seller__exact': 1})
     314        changelist = self.get_changelist(request, Book, modeladmin)
    194315
     316        # Make sure the correct queryset is returned
     317        queryset = changelist.get_query_set()
     318        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     319       
    195320        # 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
    199         choice = select_by(filterspec.choices(changelist), "display", "Yes")
     321        filterspec = changelist.get_filters()[0][3]
     322        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     323        choice = select_by(filterspec.get_output_choices(changelist), "display", "Yes")
    200324        self.assertEqual(choice['selected'], True)
    201         self.assertEqual(choice['query_string'], '?completed__exact=1')
     325        self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
     326
     327        request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
     328        changelist = self.get_changelist(request, Book, modeladmin)
     329
     330        # Make sure the correct queryset is returned
     331        queryset = changelist.get_query_set()
     332        self.assertEqual(list(queryset), [self.django_book])
     333       
     334        # Make sure the correct choice is selected
     335        filterspec = changelist.get_filters()[0][3]
     336        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     337        choice = select_by(filterspec.get_output_choices(changelist), "display", "Unknown")
     338        self.assertEqual(choice['selected'], True)
     339        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
     340       
     341    def test_SingleQueryParameterListFilter(self):
     342        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
     343       
     344        # Make sure that the first option is 'All' ---------------------------
     345       
     346        request = self.request_factory.get('/', {})
     347        changelist = self.get_changelist(request, Book, modeladmin)
     348
     349        # Make sure the correct queryset is returned
     350        queryset = changelist.get_query_set()
     351        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
     352
     353        # Make sure the correct choice is selected
     354        filterspec = changelist.get_filters()[0][1]
     355        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     356        choices = list(filterspec.get_output_choices(changelist))
     357        self.assertEqual(choices[0]['display'], u'All')
     358        self.assertEqual(choices[0]['selected'], True)
     359        self.assertEqual(choices[0]['query_string'], '?')
     360       
     361        # Make sure that one can override the query parameter name -----------
     362       
     363        request = self.request_factory.get('/', {'blah': 'the 90s'})
     364        changelist = self.get_changelist(request, Book, modeladmin)
     365       
     366        # Make sure the correct choice is selected
     367        filterspec = changelist.get_filters()[0][2]
     368        self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
     369        choices = list(filterspec.get_output_choices(changelist))
     370        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     371        self.assertEqual(choices[1]['selected'], True)
     372        self.assertEqual(choices[1]['query_string'], '?blah=the+90s')
     373       
     374        # Look for books in the 1990s ----------------------------------------
     375       
     376        request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
     377        changelist = self.get_changelist(request, Book, modeladmin)
     378
     379        # Make sure the correct queryset is returned
     380        queryset = changelist.get_query_set()
     381        self.assertEqual(list(queryset), [self.bio_book])
     382
     383        # Make sure the correct choice is selected
     384        filterspec = changelist.get_filters()[0][1]
     385        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     386        choices = list(filterspec.get_output_choices(changelist))
     387        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     388        self.assertEqual(choices[1]['selected'], True)
     389        self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
     390       
     391        # Look for books in the 2000s ----------------------------------------
     392       
     393        request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
     394        changelist = self.get_changelist(request, Book, modeladmin)
    202395
     396        # Make sure the correct queryset is returned
     397        queryset = changelist.get_query_set()
     398        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     399
     400        # Make sure the correct choice is selected
     401        filterspec = changelist.get_filters()[0][1]
     402        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     403        choices = list(filterspec.get_output_choices(changelist))
     404        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     405        self.assertEqual(choices[2]['selected'], True)
     406        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
     407       
     408        # Combine multiple filters -------------------------------------------
     409       
     410        request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
     411        changelist = self.get_changelist(request, Book, modeladmin)
     412
     413        # Make sure the correct queryset is returned
     414        queryset = changelist.get_query_set()
     415        self.assertEqual(list(queryset), [self.djangonaut_book])
     416
     417        # Make sure the correct choices are selected
     418        filterspec = changelist.get_filters()[0][1]
     419        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     420        choices = list(filterspec.get_output_choices(changelist))
     421        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     422        self.assertEqual(choices[2]['selected'], True)
     423        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     424       
     425        filterspec = changelist.get_filters()[0][0]
     426        self.assertEqual(force_unicode(filterspec.title), u'author')
     427        choice = select_by(filterspec.get_output_choices(changelist), "display", "alfred")
     428        self.assertEqual(choice['selected'], True)
     429        self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     430       
    203431class CustomUserAdmin(UserAdmin):
    204432    list_filter = ('books_authored', 'books_contributed')
    205433
    206434class BookAdmin(admin.ModelAdmin):
    207     list_filter = ('year', 'author', 'contributors')
     435    list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
     436    order_by = '-id'
     437   
     438class DecadeFilterBookAdmin(admin.ModelAdmin):
     439    list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName)
    208440    order_by = '-id'
    209 
    210 class BoolTestAdmin(admin.ModelAdmin):
    211     list_filter = ('completed',)
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..44bb957 100644
    a b from datetime import date  
    22
    33from django import forms
    44from django.conf import settings
    5 from django.contrib.admin.options import ModelAdmin, TabularInline, \
    6     HORIZONTAL, VERTICAL
     5from django.contrib.admin.options import (ModelAdmin, TabularInline,
     6    HORIZONTAL, VERTICAL)
    77from django.contrib.admin.sites import AdminSite
    88from django.contrib.admin.validation import validate
    99from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
     10from django.contrib.admin.filterspecs import (SingleQueryParameterListFilter,
     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):  
    850852            ValidationTestModelAdmin,
    851853            ValidationTestModel,
    852854        )
     855       
     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(SingleQueryParameterListFilter):
     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,)
    853902
     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