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

File 5833.custom-filterspecs.3.diff, 49.1 KB (added by Julien Phalip, 13 years ago)

Patch + tests + doc

  • django/contrib/admin/__init__.py

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..86695b1 100644
    a b  
    11# ACTION_CHECKBOX_NAME is unused, but should stay since its import from here
    22# has been referenced in documentation.
     3from django.contrib.admin.filterspecs import ListFilter, FieldListFilter,\
     4                                             SimpleListFilter
    35from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    46from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    57from django.contrib.admin.options import StackedInline, TabularInline
  • django/contrib/admin/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    index 965b32b..ac2ca72 100644
    a b from django.utils.html import escape  
    1313from django.utils.safestring import mark_safe
    1414from django.contrib.admin.util import get_model_from_relation, \
    1515    reverse_field_path, get_limit_choices_to_from_path
     16from django.template.defaultfilters import slugify
    1617import datetime
    1718
    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
     19class ListFilter(object):
     20   
     21    def __init__(self, request, params, model, model_admin):
    2322        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)
    4123
    4224    def has_output(self):
    4325        return True
    4426
    45     def choices(self, cl):
    46         raise NotImplementedError()
    47 
    48     def title(self):
    49         return self.field.verbose_name
     27    def _choices(self, cl):
     28        raise NotImplementedError
     29
     30    def get_title(self):
     31        raise NotImplementedError
     32
     33    def get_query_set(self, changelist, queryset):
     34        return queryset
     35   
     36    def _consumed_params(self):
     37        """
     38        Return a list of parameters to consume from the change list
     39        querystring.
     40       
     41        Override this for non-field based ListFilter subclasses in order
     42        to consume custom GET parameters, as any GET parameters that are not
     43        consumed and are not a field name raises an exception.
     44        """
     45        return []
    5046
    5147    def output(self, cl):
    5248        t = []
    5349        if self.has_output():
    54             t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
     50            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.get_title()))
    5551
    56             for choice in self.choices(cl):
     52            for choice in self._choices(cl):
    5753                t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
    5854                    ((choice['selected'] and ' class="selected"' or ''),
    5955                     iri_to_uri(choice['query_string']),
    6056                     choice['display']))
    6157            t.append('</ul>\n\n')
    6258        return mark_safe("".join(t))
     59   
    6360
    64 class RelatedFilterSpec(FilterSpec):
    65     def __init__(self, f, request, params, model, model_admin,
     61
     62           
     63           
     64class SimpleListFilter(ListFilter):
     65   
     66    def __init__(self, request, params, model, model_admin):
     67        super(SimpleListFilter, self).__init__(request, params, model, model_admin)
     68        self.lookup_kwarg = self.get_query_parameter_name()
     69        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     70        self.lookup_choices = self.get_choices(request)
     71   
     72    def has_output(self):
     73        return len(self.lookup_choices) > 0
     74       
     75    def get_query_parameter_name(self):
     76        """
     77            Returns the parameter that should be used in the query string
     78            for that filter. Defaults to the title, slugified.
     79        """
     80        return slugify(self.get_title())
     81   
     82    def get_value(self):
     83        """
     84            Returns the value given in the query string for this filter,
     85            if any. Returns None otherwise.
     86        """
     87        return self.params.get(self.lookup_kwarg, None)
     88
     89    def get_choices(self, request):
     90        """
     91            Must be overriden to return a list of tuples (value, verbose value)
     92        """
     93        raise NotImplementedError
     94
     95    def _consumed_params(self):
     96        return [self.lookup_kwarg]
     97
     98    def _choices(self, cl):
     99        yield {'selected': self.lookup_val is None,
     100               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
     101               'display': _('All')}
     102        for k, v in self.lookup_choices:
     103            yield {'selected': self.lookup_val == k,
     104                   'query_string': cl.get_query_string(
     105                                   {self.lookup_kwarg: k},
     106                                   []),
     107                   'display': v}
     108
     109
     110           
     111class FieldListFilter(ListFilter):
     112    _field_list_filters = []
     113
     114    def __init__(self, field, request, params, model, model_admin, \
     115                 field_path=None):
     116        super(FieldListFilter, self).__init__(request, params, model, \
     117                                              model_admin)
     118        self.field = field
     119        if field_path is None:
     120            if isinstance(field, models.related.RelatedObject):
     121                self.field_path = field.var_name
     122            else:
     123                self.field_path = field.name
     124        else:
     125            self.field_path = field_path
     126
     127    def get_title(self):
     128        return self.field.verbose_name
     129
     130    @classmethod
     131    def register(cls, test, list_filter_class, insert_first=False):
     132        if insert_first:
     133            # This is to allow overriding default filters for certain types
     134            # of fields with custom ones. The first found in the list is used
     135            # in priority.
     136            cls._field_list_filters.insert(0, (test, list_filter_class))
     137        else:
     138            cls._field_list_filters.append((test, list_filter_class))
     139   
     140    @classmethod
     141    def create(cls, field, request, params, model, model_admin, field_path=None):
     142        for test, list_filter_class in cls._field_list_filters:
     143            if test(field):
     144                return list_filter_class(field, request, params, model, model_admin,
     145                               field_path=field_path)
     146
     147       
     148class RelatedFieldListFilter(FieldListFilter):
     149    def __init__(self, field, request, params, model, model_admin,
    66150                 field_path=None):
    67         super(RelatedFilterSpec, self).__init__(
    68             f, request, params, model, model_admin, field_path=field_path)
     151        super(RelatedFieldListFilter, self).__init__(
     152            field, request, params, model, model_admin, field_path=field_path)
    69153
    70         other_model = get_model_from_relation(f)
    71         if isinstance(f, (models.ManyToManyField,
     154        other_model = get_model_from_relation(field)
     155        if isinstance(field, (models.ManyToManyField,
    72156                          models.related.RelatedObject)):
    73157            # no direct field on this model, get name from other model
    74158            self.lookup_title = other_model._meta.verbose_name
    75159        else:
    76             self.lookup_title = f.verbose_name # use field name
     160            self.lookup_title = field.verbose_name # use field name
    77161        rel_name = other_model._meta.pk.name
    78162        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
    79163        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
    80164        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    81165        self.lookup_val_isnull = request.GET.get(
    82166                                      self.lookup_kwarg_isnull, None)
    83         self.lookup_choices = f.get_choices(include_blank=False)
     167        self.lookup_choices = field.get_choices(include_blank=False)
    84168
    85169    def has_output(self):
    86170        if isinstance(self.field, models.related.RelatedObject) \
    class RelatedFilterSpec(FilterSpec):  
    91175            extra = 0
    92176        return len(self.lookup_choices) + extra > 1
    93177
    94     def title(self):
     178    def get_title(self):
    95179        return self.lookup_title
    96180
    97     def choices(self, cl):
     181    def _choices(self, cl):
    98182        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    99183        yield {'selected': self.lookup_val is None
    100184                           and not self.lookup_val_isnull,
    class RelatedFilterSpec(FilterSpec):  
    117201                                   [self.lookup_kwarg]),
    118202                   'display': EMPTY_CHANGELIST_VALUE}
    119203
    120 FilterSpec.register(lambda f: (
     204FieldListFilter.register(lambda f: (
    121205        hasattr(f, 'rel') and bool(f.rel) or
    122         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     206        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
    123207
    124 class BooleanFieldFilterSpec(FilterSpec):
     208class BooleanFieldListFilter(FieldListFilter):
    125209    def __init__(self, f, request, params, model, model_admin,
    126210                 field_path=None):
    127         super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
     211        super(BooleanFieldListFilter, self).__init__(f, request, params, model,
    128212                                                     model_admin,
    129213                                                     field_path=field_path)
    130214        self.lookup_kwarg = '%s__exact' % self.field_path
    class BooleanFieldFilterSpec(FilterSpec):  
    132216        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133217        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134218
    135     def title(self):
    136         return self.field.verbose_name
    137 
    138     def choices(self, cl):
     219    def _choices(self, cl):
    139220        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    140221            yield {'selected': self.lookup_val == v and not self.lookup_val2,
    141222                   'query_string': cl.get_query_string(
    class BooleanFieldFilterSpec(FilterSpec):  
    149230                                   [self.lookup_kwarg]),
    150231                   'display': _('Unknown')}
    151232
    152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
     233FieldListFilter.register(lambda f: isinstance(f, models.BooleanField)
    153234                              or isinstance(f, models.NullBooleanField),
    154                                  BooleanFieldFilterSpec)
     235                                 BooleanFieldListFilter)
    155236
    156 class ChoicesFilterSpec(FilterSpec):
     237class ChoicesFieldListFilter(FieldListFilter):
    157238    def __init__(self, f, request, params, model, model_admin,
    158239                 field_path=None):
    159         super(ChoicesFilterSpec, self).__init__(f, request, params, model,
     240        super(ChoicesFieldListFilter, self).__init__(f, request, params, model,
    160241                                                model_admin,
    161242                                                field_path=field_path)
    162243        self.lookup_kwarg = '%s__exact' % self.field_path
    163         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     244        self.lookup_val = request.GET.get(self.lookup_kwarg)
    164245
    165     def choices(self, cl):
     246    def _choices(self, cl):
    166247        yield {'selected': self.lookup_val is None,
    167248               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
    168249               'display': _('All')}
    class ChoicesFilterSpec(FilterSpec):  
    172253                                    {self.lookup_kwarg: k}),
    173254                    'display': v}
    174255
    175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     256FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
    176257
    177 class DateFieldFilterSpec(FilterSpec):
     258class DateFieldListFilter(FieldListFilter):
    178259    def __init__(self, f, request, params, model, model_admin,
    179260                 field_path=None):
    180         super(DateFieldFilterSpec, self).__init__(f, request, params, model,
     261        super(DateFieldListFilter, self).__init__(f, request, params, model,
    181262                                                  model_admin,
    182263                                                  field_path=field_path)
    183264
    class DateFieldFilterSpec(FilterSpec):  
    205286            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
    206287        )
    207288
    208     def title(self):
    209         return self.field.verbose_name
    210 
    211     def choices(self, cl):
     289    def _choices(self, cl):
    212290        for title, param_dict in self.links:
    213291            yield {'selected': self.date_params == param_dict,
    214292                   'query_string': cl.get_query_string(
    class DateFieldFilterSpec(FilterSpec):  
    216294                                   [self.field_generic]),
    217295                   'display': title}
    218296
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     297FieldListFilter.register(lambda f: isinstance(f, models.DateField),
     298                              DateFieldListFilter)
    221299
    222300
    223301# 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):
     302# if a field is eligible to use the BooleanFieldListFilter, that'd be much
     303# more appropriate, and the AllValuesFieldListFilter won't get used for it.
     304class AllValuesFieldListFilter(FieldListFilter):
    227305    def __init__(self, f, request, params, model, model_admin,
    228306                 field_path=None):
    229         super(AllValuesFilterSpec, self).__init__(f, request, params, model,
     307        super(AllValuesFieldListFilter, self).__init__(f, request, params, model,
    230308                                                  model_admin,
    231309                                                  field_path=field_path)
    232310        self.lookup_kwarg = self.field_path
    class AllValuesFilterSpec(FilterSpec):  
    245323        self.lookup_choices = \
    246324            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
    247325
    248     def title(self):
    249         return self.field.verbose_name
    250 
    251     def choices(self, cl):
     326    def _choices(self, cl):
    252327        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    253328        yield {'selected': self.lookup_val is None
    254329                           and self.lookup_val_isnull is None,
    class AllValuesFilterSpec(FilterSpec):  
    276351                                    [self.lookup_kwarg]),
    277352                    'display': EMPTY_CHANGELIST_VALUE}
    278353
    279 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     354FieldListFilter.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..6bac03b 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.get_title(), 'choices' : list(spec._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..278b45d 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 ListFilter, 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 methods of specifying a filter:
     60            #   1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
     61            #   2: ('field', SomeFieldListFilter) - a field-based list filter class
     62            #   3: SomeListFilter - a non-field list filter class
     63            if callable(item) and not isinstance(item, models.Field):
     64                # If item is option 3, it should be a ListFilter, but not a FieldListFilter
     65                if not issubclass(item, ListFilter) or issubclass(item, FieldListFilter):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not of type ListFilter."
     68                            % (cls.__name__, idx, item.__name__))
     69            else:
     70                try:
     71                    # Check for option #2 (tuple)
     72                    field, list_filter_class = item
     73                except (TypeError, ValueError):
     74                    # item is option #1
     75                    field = item
     76                else:
     77                    # item is option #2
     78                    if not issubclass(list_filter_class, FieldListFilter):
     79                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
     80                            " is '%s' which is not of type FieldListFilter."
     81                            % (cls.__name__, idx, list_filter_class.__name__))
     82                # Validate the field string
     83                try:
     84                    get_fields_from_path(model, field)
     85                except (NotRelationField, FieldDoesNotExist), e:
     86                    raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
     87                            " which does not refer to a Field."
     88                            % (cls.__name__, idx, field))
    6689
    6790    # list_per_page = 100
    6891    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 170d168..de037e6 100644
    a b  
    1 from django.contrib.admin.filterspecs import FilterSpec
     1from django.contrib.admin.filterspecs import ListFilter, 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
    class ChangeList(object):  
    5959            self.list_editable = ()
    6060        else:
    6161            self.list_editable = list_editable
     62        self.filter_specs, self.has_filters = self.get_filters(request)
    6263        self.order_field, self.order_type = self.get_ordering()
    6364        self.query = request.GET.get(SEARCH_VAR, '')
    6465        self.query_set = self.get_query_set()
    6566        self.get_results(request)
    6667        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))
    67         self.filter_specs, self.has_filters = self.get_filters(request)
    6868        self.pk_attname = self.lookup_opts.pk.attname
    6969
    7070    def get_filters(self, request):
    7171        filter_specs = []
    7272        if self.list_filter:
    73             for filter_name in self.list_filter:
    74                 field = get_fields_from_path(self.model, filter_name)[-1]
    75                 spec = FilterSpec.create(field, request, self.params,
    76                                          self.model, self.model_admin,
    77                                          field_path=filter_name)
     73            for item in self.list_filter:
     74                if callable(item):
     75                    # This is simply a custom ListFilter class.
     76                    spec = item(request, self.params, self.model, self.model_admin)
     77                else:
     78                    field_path = None
     79                    try:
     80                        # This is custom FieldListFilter class for a given field.
     81                        field, field_list_filter_class = item
     82                    except (TypeError, ValueError):
     83                        # This is simply a field name, so use the default
     84                        # FieldListFilter class that has been registered for
     85                        # the type of the given field.
     86                        field, field_list_filter_class = item, FieldListFilter.create
     87                    if not isinstance(field, models.Field):
     88                        field_path = field
     89                        field = get_fields_from_path(self.model, field_path)[-1]
     90                    spec = field_list_filter_class(field, request, self.params, self.model,
     91                            self.model_admin, field_path=field_path)
    7892                if spec and spec.has_output():
    7993                    filter_specs.append(spec)
    8094        return filter_specs, bool(filter_specs)
    class ChangeList(object):  
    166180            order_type = params[ORDER_TYPE_VAR]
    167181        return order_field, order_type
    168182
     183    def apply_list_filters(self, qs, lookup_params):
     184        for filter_spec in self.filter_specs:
     185            new_qs = filter_spec.get_query_set(self, qs)
     186            if new_qs is not None and new_qs is not False:
     187                qs = new_qs
     188                # Only consume params if we got a new queryset
     189                for param in filter_spec._consumed_params():
     190                    try:
     191                        del lookup_params[param]
     192                    except KeyError:
     193                        pass
     194        return qs
     195   
    169196    def get_query_set(self):
    170197        use_distinct = False
    171198
    class ChangeList(object):  
    187214                field_name = key.split('__', 1)[0]
    188215                try:
    189216                    f = self.lookup_opts.get_field_by_name(field_name)[0]
     217                    if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
     218                        use_distinct = True
    190219                except models.FieldDoesNotExist:
    191                     raise IncorrectLookupParameters
    192                 if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
    193                     use_distinct = True
     220                    # It might be for a non-field custom filter specs.
     221                    pass
    194222
    195223            # if key ends with __in, split parameter into separate values
    196224            if key.endswith('__in'):
    class ChangeList(object):  
    209237                raise SuspiciousOperation(
    210238                    "Filtering by %s not allowed" % key
    211239                )
    212 
     240        # Let every list filter modify the qs and params to its liking
     241        qs = self.apply_list_filters(qs, lookup_params)
     242       
    213243        # Apply lookup parameters from the query string.
    214244        try:
    215245            qs = qs.filter(**lookup_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 415e1fe..a8c6b8b 100644
    a b subclass::  
    512512    .. note::
    513513
    514514        ``list_editable`` interacts with a couple of other options in
    515         particular ways; you should note the following rules:
     515        particular ways; you should note the following rules::
    516516
    517517            * Any field in ``list_editable`` must also be in ``list_display``.
    518518              You can't edit a field that's not displayed!
    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``.
     528    .. versionchanged:: Development version
    532529
    533     This example, taken from the ``django.contrib.auth.models.User`` model,
    534     shows how both ``list_display`` and ``list_filter`` work::
     530    Set ``list_filter`` to activate filters in the right sidebar of the change
     531    list page of the admin. This should be a list of elements, where each
     532    element should be of one of the following types::
     533   
     534        * a field name, where the specified field should be either a
     535          ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
     536          ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``.
     537
     538            .. versionadded:: 1.3
     539
     540                Field names in ``list_filter`` can also span relations
     541                using the ``__`` lookup, for example::
     542               
     543                    class UserAdminWithLookup(UserAdmin):
     544                        list_filter = ('groups__name')
     545
     546        * a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`,
     547          where you need to override a few methods::
     548       
     549            from django.contrib.admin import SimpleListFilter
     550            from django.db.models import Q
     551           
     552            class DecadeBornListFilter(SimpleListFilter):
     553               
     554                def get_title(self):
     555                    # Human-readable title which will be displayed in the
     556                    # right sidebar just above the filter options.
     557                    return u'decade born'
     558               
     559                def get_query_parameter_name(self):
     560                    # This is the code name for the filter that will be used in
     561                    # the url query. Overriding this method is optional (by default,
     562                    # a slugified version of the title will automatically be used,
     563                    # that is, 'decade-born' in this example).
     564                    return u'decade'
     565               
     566                def get_choices(self, request):
     567                    # Return a list of tuples. The first element in each tuple
     568                    # is the coded value for the option that will appear in the
     569                    # url query. The second element is the human-readable name
     570                    # for the option that will appear in the right sidebar. You
     571                    # may specify as many choices as you like, and you may even
     572                    # vary the list of choices depending on the HttpRequest
     573                    # object provided as argument to this method.
     574                    return (
     575                        (u'80s', u'in the eighties'),
     576                        (u'other', u'other'),
     577                    )
     578               
     579                def get_query_set(self, changelist, queryset):
     580                    # First, retrieve the requested value (either '80s' or 'other').
     581                    decade = self.get_value()
     582                    # Then decide how to filter the queryset based on that value.
     583                    if decade == u'80s':
     584                        return queryset.filter(birthday__year__gte=1980,
     585                                               birthday__year__lte=1989)
     586                    if decade == u'other':
     587                        return queryset.filter(Q(year__lte=1979) |
     588                                               Q(year__gte=1990)
     589                    # Always return the unchanged queryset by default
     590                    return queryset
     591   
     592            class PersonAdmin(ModelAdmin):
     593                list_filter = (DecadeBornListFilter,)
     594       
     595        * a class inheriting from :mod:`django.contrib.admin.FieldListFilter`,
     596          in case you would like to change the default behaviour of a filter
     597          for a given field. For example, you may define a custom boolean
     598          filter called ``CustomBooleanFieldListFiler``. You may then register
     599          it using a tuple to bind it to a given field::
     600         
     601            class PersonAdmin(ModelAdmin):
     602                list_filter = (('is_staff', CustomBooleanFieldListFiler),)
     603       
     604          Note that this method is far more complex than simply using a field
     605          name or a ``SimpleListFilter`` class, as there currently is no simple
     606          way available to manipulate a ``FieldListFilter``. You may, however,
     607          find some useful examples with the built-in filters defined in
     608          :mod:`django.contrib.admin.filterspecs`.
     609       
     610    Finally, the following example, taken from the ``django.contrib.auth.models.User``
     611    model, shows how both ``list_display`` and ``list_filter`` work::
    535612
    536613        class UserAdmin(admin.ModelAdmin):
    537614            list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
    subclass::  
    543620
    544621    (This example also has ``search_fields`` defined. See below.)
    545622
    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')
    552 
    553623.. attribute:: ModelAdmin.list_per_page
    554624
    555625    Set ``list_per_page`` to control how many items appear on each paginated
  • tests/regressiontests/admin_filterspecs/tests.py

    diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
    index 8b9e734..7bbc9dd 100644
    a b from django.contrib.auth.models import User  
    55from django.contrib import admin
    66from django.contrib.admin.views.main import ChangeList
    77from django.utils.encoding import force_unicode
     8from django.contrib.admin.filterspecs import (SimpleListFilter,
     9    BooleanFieldListFilter, FieldListFilter)
    810
    911from models import Book, BoolTest
    1012
    1113def select_by(dictlist, key, value):
    1214    return [x for x in dictlist if x[key] == value][0]
    1315
    14 class FilterSpecsTests(TestCase):
     16
     17
     18class DecadeListFilter(SimpleListFilter):
     19   
     20    def get_title(self):
     21        return u'publication decade'
     22   
     23    def get_choices(self, request):
     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, cl, qs):
     31        decade = self.get_value()
     32        if decade == u'the 90s':
     33            return qs.filter(year__gte=1990, year__lte=1999)
     34        if decade == u'the 00s':
     35            return qs.filter(year__gte=2000, year__lte=2009)
     36        return qs
     37
     38
     39class ListFiltersTests(TestCase):
    1540
    1641    def setUp(self):
    1742        # Users
    1843        self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
    1944        self.bob = User.objects.create_user('bob', 'bob@example.com')
    20         lisa = User.objects.create_user('lisa', 'lisa@example.com')
     45        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
    2146
    2247        #Books
     48        self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred)
    2349        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred)
    2450        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()
     51        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
     52        self.gipsy_book.contributors = [self.bob, self.lisa]
     53        self.gipsy_book.save()
    2854
    2955        # BoolTests
    3056        self.trueTest = BoolTest.objects.create(completed=True)
    class FilterSpecsTests(TestCase):  
    3864            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    3965            modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
    4066
    41     def test_AllValuesFilterSpec(self):
     67    def test_AllValuesFieldListFilter(self):
    4268        modeladmin = BookAdmin(Book, admin.site)
    4369
    4470        request = self.request_factory.get('/', {'year__isnull': 'True'})
    class FilterSpecsTests(TestCase):  
    4975
    5076        # Make sure the last choice is None and is selected
    5177        filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
    53         choices = list(filterspec.choices(changelist))
     78        self.assertEqual(force_unicode(filterspec.get_title()), u'year')
     79        choices = list(filterspec._choices(changelist))
    5480        self.assertEqual(choices[-1]['selected'], True)
    5581        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    5682
    class FilterSpecsTests(TestCase):  
    5985
    6086        # Make sure the correct choice is selected
    6187        filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
    63         choices = list(filterspec.choices(changelist))
     88        self.assertEqual(force_unicode(filterspec.get_title()), u'year')
     89        choices = list(filterspec._choices(changelist))
    6490        self.assertEqual(choices[2]['selected'], True)
    6591        self.assertEqual(choices[2]['query_string'], '?year=2002')
    6692
    67     def test_RelatedFilterSpec_ForeignKey(self):
     93    def test_RelatedFieldListFilter_ForeignKey(self):
    6894        modeladmin = BookAdmin(Book, admin.site)
    6995
    7096        request = self.request_factory.get('/', {'author__isnull': 'True'})
    class FilterSpecsTests(TestCase):  
    77103
    78104        # Make sure the last choice is None and is selected
    79105        filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
    81         choices = list(filterspec.choices(changelist))
     106        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
     107        choices = list(filterspec._choices(changelist))
    82108        self.assertEqual(choices[-1]['selected'], True)
    83109        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    84110
    class FilterSpecsTests(TestCase):  
    87113
    88114        # Make sure the correct choice is selected
    89115        filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     116        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
    91117        # order of choices depends on User model, which has no order
    92         choice = select_by(filterspec.choices(changelist), "display", "alfred")
     118        choice = select_by(filterspec._choices(changelist), "display", "alfred")
    93119        self.assertEqual(choice['selected'], True)
    94120        self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
    95121
    96     def test_RelatedFilterSpec_ManyToMany(self):
     122    def test_RelatedFieldListFilter_ManyToMany(self):
    97123        modeladmin = BookAdmin(Book, admin.site)
    98124
    99125        request = self.request_factory.get('/', {'contributors__isnull': 'True'})
    class FilterSpecsTests(TestCase):  
    104130
    105131        # Make sure the last choice is None and is selected
    106132        filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
    108         choices = list(filterspec.choices(changelist))
     133        self.assertEqual(force_unicode(filterspec.get_title()), u'user')
     134        choices = list(filterspec._choices(changelist))
    109135        self.assertEqual(choices[-1]['selected'], True)
    110136        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    111137
    class FilterSpecsTests(TestCase):  
    114140
    115141        # Make sure the correct choice is selected
    116142        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")
     143        self.assertEqual(force_unicode(filterspec.get_title()), u'user')
     144        choice = select_by(filterspec._choices(changelist), "display", "bob")
    119145        self.assertEqual(choice['selected'], True)
    120146        self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
    121147
    122148
    123     def test_RelatedFilterSpec_reverse_relationships(self):
     149    def test_RelatedFieldListFilter_reverse_relationships(self):
    124150        modeladmin = CustomUserAdmin(User, admin.site)
    125151
    126152        # FK relationship -----
    class FilterSpecsTests(TestCase):  
    132158
    133159        # Make sure the last choice is None and is selected
    134160        filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
    136         choices = list(filterspec.choices(changelist))
     161        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     162        choices = list(filterspec._choices(changelist))
    137163        self.assertEqual(choices[-1]['selected'], True)
    138164        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    139165
    class FilterSpecsTests(TestCase):  
    142168
    143169        # Make sure the correct choice is selected
    144170        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)
     171        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     172        choice = select_by(filterspec._choices(changelist), "display", self.bio_book.title)
    147173        self.assertEqual(choice['selected'], True)
    148174        self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
    149175
    class FilterSpecsTests(TestCase):  
    156182
    157183        # Make sure the last choice is None and is selected
    158184        filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
    160         choices = list(filterspec.choices(changelist))
     185        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     186        choices = list(filterspec._choices(changelist))
    161187        self.assertEqual(choices[-1]['selected'], True)
    162188        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    163189
    class FilterSpecsTests(TestCase):  
    166192
    167193        # Make sure the correct choice is selected
    168194        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)
     195        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     196        choice = select_by(filterspec._choices(changelist), "display", self.django_book.title)
    171197        self.assertEqual(choice['selected'], True)
    172198        self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
    173199
    174     def test_BooleanFilterSpec(self):
     200    def test_BooleanFieldListFilter(self):
    175201        modeladmin = BoolTestAdmin(BoolTest, admin.site)
    176 
     202        self.verify_BooleanFieldListFilter(modeladmin)
     203       
     204    def test_BooleanFieldListFilter_Tuple(self):
     205        modeladmin = BoolTupleTestAdmin(BoolTest, admin.site)
     206        self.verify_BooleanFieldListFilter(modeladmin)
     207       
     208    def verify_BooleanFieldListFilter(self, modeladmin):
    177209        request = self.request_factory.get('/')
    178210        changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links,
    179211            modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
    class FilterSpecsTests(TestCase):  
    184216
    185217        # Make sure the last choice is None and is selected
    186218        filterspec = changelist.get_filters(request)[0][0]
    187         self.assertEqual(force_unicode(filterspec.title()), u'completed')
    188         choices = list(filterspec.choices(changelist))
     219        self.assertEqual(force_unicode(filterspec.get_title()), u'completed')
     220        choices = list(filterspec._choices(changelist))
    189221        self.assertEqual(choices[-1]['selected'], False)
    190222        self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
    191223
    class FilterSpecsTests(TestCase):  
    194226
    195227        # Make sure the correct choice is selected
    196228        filterspec = changelist.get_filters(request)[0][0]
    197         self.assertEqual(force_unicode(filterspec.title()), u'completed')
     229        self.assertEqual(force_unicode(filterspec.get_title()), u'completed')
    198230        # order of choices depends on User model, which has no order
    199         choice = select_by(filterspec.choices(changelist), "display", "Yes")
     231        choice = select_by(filterspec._choices(changelist), "display", "Yes")
    200232        self.assertEqual(choice['selected'], True)
    201233        self.assertEqual(choice['query_string'], '?completed__exact=1')
    202234
     235    def test_SimpleListFilter(self):
     236        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
     237       
     238        # Make sure that the first option is 'All' -------
     239       
     240        request = self.request_factory.get('/', {})
     241        changelist = self.get_changelist(request, Book, modeladmin)
     242
     243        # Make sure the correct queryset is returned
     244        queryset = changelist.get_query_set()
     245        self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
     246
     247        # Make sure the correct choice is selected
     248        filterspec = changelist.get_filters(request)[0][1]
     249        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     250        choices = list(filterspec._choices(changelist))
     251        self.assertEqual(choices[0]['display'], u'All')
     252        self.assertEqual(choices[0]['selected'], True)
     253        self.assertEqual(choices[0]['query_string'], '?')
     254       
     255        # Look for books in the 1990s --------
     256       
     257        request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
     258        changelist = self.get_changelist(request, Book, modeladmin)
     259
     260        # Make sure the correct queryset is returned
     261        queryset = changelist.get_query_set()
     262        self.assertEqual(list(queryset), [self.bio_book])
     263
     264        # Make sure the correct choice is selected
     265        filterspec = changelist.get_filters(request)[0][1]
     266        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     267        choices = list(filterspec._choices(changelist))
     268        self.assertEqual(choices[1]['display'], u'the 1990\'s')
     269        self.assertEqual(choices[1]['selected'], True)
     270        self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
     271       
     272        # Look for books in the 2000s --------
     273       
     274        request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
     275        changelist = self.get_changelist(request, Book, modeladmin)
     276
     277        # Make sure the correct queryset is returned
     278        queryset = changelist.get_query_set()
     279        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     280
     281        # Make sure the correct choice is selected
     282        filterspec = changelist.get_filters(request)[0][1]
     283        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     284        choices = list(filterspec._choices(changelist))
     285        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     286        self.assertEqual(choices[2]['selected'], True)
     287        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
     288       
     289        # Combine multiple filters --------
     290       
     291        request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
     292        changelist = self.get_changelist(request, Book, modeladmin)
     293
     294        # Make sure the correct queryset is returned
     295        queryset = changelist.get_query_set()
     296        self.assertEqual(list(queryset), [self.djangonaut_book])
     297
     298        # Make sure the correct choices are selected
     299        filterspec = changelist.get_filters(request)[0][1]
     300        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     301        choices = list(filterspec._choices(changelist))
     302        self.assertEqual(choices[2]['display'], u'the 2000\'s')
     303        self.assertEqual(choices[2]['selected'], True)
     304        self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     305       
     306        filterspec = changelist.get_filters(request)[0][0]
     307        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
     308        choice = select_by(filterspec._choices(changelist), "display", "alfred")
     309        self.assertEqual(choice['selected'], True)
     310        self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
     311       
    203312class CustomUserAdmin(UserAdmin):
    204313    list_filter = ('books_authored', 'books_contributed')
    205314
    class BookAdmin(admin.ModelAdmin):  
    209318
    210319class BoolTestAdmin(admin.ModelAdmin):
    211320    list_filter = ('completed',)
     321
     322class BoolTupleTestAdmin(admin.ModelAdmin):
     323    list_filter = (('completed', BooleanFieldListFilter),)
     324   
     325class DecadeFilterBookAdmin(admin.ModelAdmin):
     326    list_filter = ('author', DecadeListFilter)
     327    order_by = '-id'
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..6f31e7f 100644
    a b from datetime import date  
    22
    33from django import forms
    44from django.conf import settings
    5 from django.contrib.admin.options import ModelAdmin, TabularInline, \
    6     HORIZONTAL, VERTICAL
     5from django.contrib.admin.options import (ModelAdmin, TabularInline,
     6    HORIZONTAL, VERTICAL)
    77from django.contrib.admin.sites import AdminSite
    88from django.contrib.admin.validation import validate
    99from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
     10from django.contrib.admin.filterspecs import (SimpleListFilter,
     11     BooleanFieldListFilter)
    1012from django.core.exceptions import ImproperlyConfigured
    1113from django.forms.models import BaseModelFormSet
    1214from django.forms.widgets import Select
    1315from django.test import TestCase
    1416from django.utils import unittest
    1517
    16 from models import Band, Concert, ValidationTestModel, \
    17     ValidationTestInlineModel
     18from models import (Band, Concert, ValidationTestModel,
     19    ValidationTestInlineModel)
    1820
    1921
    2022# None of the following tests really depend on the content of the request,
    class ValidationTests(unittest.TestCase):  
    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 of type ListFilter.",
     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),)
    853891
     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        # Valid declarations below -----------
     901       
    854902        class ValidationTestModelAdmin(ModelAdmin):
    855             list_filter = ('is_active',)
     903            list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
    856904
    857905        validate(ValidationTestModelAdmin, ValidationTestModel)
    858906
Back to Top