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

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

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..d725568 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 ListFilter, FieldListFilter
    88
    99def autodiscover():
    1010    """
  • django/contrib/admin/filterspecs.py

    diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
    index 965b32b..39288f0 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
    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)
    4128
     29    def should_be_used(self):
     30        """
     31            Returns True if the filter should be used, based on the parameters
     32            given in the query string.
     33        """
     34        for p in self.used_params():
     35            if p in self.params:
     36                return True
     37        return False
     38       
    4239    def has_output(self):
    43         return True
     40        raise NotImplementedError
    4441
    45     def choices(self, cl):
    46         raise NotImplementedError()
     42    def _choices(self, cl):
     43        raise NotImplementedError
    4744
    48     def title(self):
    49         return self.field.verbose_name
     45    def get_query_set(self, request, queryset):
     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
    5054
    5155    def output(self, cl):
    5256        t = []
    5357        if self.has_output():
    54             t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
     58            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title))
    5559
    56             for choice in self.choices(cl):
     60            for choice in self._choices(cl):
    5761                t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
    5862                    ((choice['selected'] and ' class="selected"' or ''),
    5963                     iri_to_uri(choice['query_string']),
    6064                     choice['display']))
    6165            t.append('</ul>\n\n')
    6266        return mark_safe("".join(t))
     67   
     68
     69
     70           
     71           
     72class ListFilter(ListFilterBase):
     73    """
     74        API to make the creation of a custom non-field list filter as simple
     75        and easy as possible.
     76    """
     77   
     78    # Parameter that should be used in the query string for that filter.
     79    # Defaults to the title, slugified.
     80    query_parameter_name = None
     81   
     82    def __init__(self, request, params, model, model_admin):
     83        super(ListFilter, self).__init__(request, params, model, model_admin)
     84        if self.query_parameter_name is None:
     85            self.query_parameter_name = slugify(self.title)
     86        self.lookup_choices = self.get_choices(request)
     87   
     88    def has_output(self):
     89        return len(self.lookup_choices) > 0
     90   
     91    def get_value(self):
     92        """
     93            Returns the value given in the query string for this filter,
     94            if any. Returns None otherwise.
     95        """
     96        return self.params.get(self.query_parameter_name, None)
     97
     98    def get_choices(self, request):
     99        """
     100            Must be overriden to return a list of tuples (value, verbose value)
     101        """
     102        raise NotImplementedError
     103
     104    def used_params(self):
     105        return [self.query_parameter_name]
     106
     107    def _choices(self, cl):
     108        yield {'selected': self.get_value() is None,
     109               'query_string': cl.get_query_string({}, [self.query_parameter_name]),
     110               'display': _('All')}
     111        for k, v in self.lookup_choices:
     112            yield {'selected': self.get_value() == k,
     113                   'query_string': cl.get_query_string(
     114                                   {self.query_parameter_name: k},
     115                                   []),
     116                   'display': v}
     117
     118
     119           
     120class FieldListFilter(ListFilterBase):
     121    _field_list_filters = []
     122    _take_priority_index = 0
     123   
     124    def __init__(self, field, request, params, model, model_admin, \
     125                 field_path):
     126        self.field = field
     127        self.field_path = field_path
     128        self.title = field_path
     129        super(FieldListFilter, self).__init__(request, params, model, \
     130                                              model_admin)
    63131
    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)
     132    def has_output(self):
     133        return True
     134       
     135    def get_query_set(self, request, queryset):
     136        for p in self.used_params():
     137            if p in self.params:
     138                return queryset.filter(**{p: self.params[p]})
     139       
     140    @classmethod
     141    def register(cls, test, list_filter_class, take_priority=False):
     142        if take_priority:
     143            # This is to allow overriding the default filters for certain types
     144            # of fields with some custom filters. The first found in the list
     145            # is used in priority.
     146            cls._field_list_filters.insert(cls._take_priority_index, (test, list_filter_class))
     147            _take_priority_index += 1
     148        else:
     149            cls._field_list_filters.append((test, list_filter_class))
     150   
     151    @classmethod
     152    def create(cls, field, request, params, model, model_admin, field_path):
     153        for test, list_filter_class in cls._field_list_filters:
     154            if test(field):
     155                return list_filter_class(field, request, params, model, model_admin,
     156                               field_path=field_path)
    69157
    70         other_model = get_model_from_relation(f)
    71         if isinstance(f, (models.ManyToManyField,
     158       
     159class RelatedFieldListFilter(FieldListFilter):
     160    def __init__(self, field, request, params, model, model_admin,
     161                 field_path):
     162        super(RelatedFieldListFilter, self).__init__(
     163            field, request, params, model, model_admin, field_path)
     164
     165        other_model = get_model_from_relation(field)
     166        if isinstance(field, (models.ManyToManyField,
    72167                          models.related.RelatedObject)):
    73168            # no direct field on this model, get name from other model
    74169            self.lookup_title = other_model._meta.verbose_name
    75170        else:
    76             self.lookup_title = f.verbose_name # use field name
     171            self.lookup_title = field.verbose_name # use field name
    77172        rel_name = other_model._meta.pk.name
    78173        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
    79174        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
    80175        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    81176        self.lookup_val_isnull = request.GET.get(
    82177                                      self.lookup_kwarg_isnull, None)
    83         self.lookup_choices = f.get_choices(include_blank=False)
     178        self.lookup_choices = field.get_choices(include_blank=False)
     179        self.title = self.lookup_title
    84180
    85181    def has_output(self):
    86182        if isinstance(self.field, models.related.RelatedObject) \
    class RelatedFilterSpec(FilterSpec): 
    91187            extra = 0
    92188        return len(self.lookup_choices) + extra > 1
    93189
    94     def title(self):
    95         return self.lookup_title
    96 
    97     def choices(self, cl):
     190    def used_params(self):
     191        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
     192   
     193    def _choices(self, cl):
    98194        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    99195        yield {'selected': self.lookup_val is None
    100196                           and not self.lookup_val_isnull,
    class RelatedFilterSpec(FilterSpec): 
    117213                                   [self.lookup_kwarg]),
    118214                   'display': EMPTY_CHANGELIST_VALUE}
    119215
    120 FilterSpec.register(lambda f: (
     216FieldListFilter.register(lambda f: (
    121217        hasattr(f, 'rel') and bool(f.rel) or
    122         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     218        isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
    123219
    124 class BooleanFieldFilterSpec(FilterSpec):
     220class BooleanFieldListFilter(FieldListFilter):
    125221    def __init__(self, f, request, params, model, model_admin,
    126                  field_path=None):
    127         super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
     222                 field_path):
     223        super(BooleanFieldListFilter, self).__init__(f, request, params, model,
    128224                                                     model_admin,
    129                                                      field_path=field_path)
     225                                                     field_path)
    130226        self.lookup_kwarg = '%s__exact' % self.field_path
    131227        self.lookup_kwarg2 = '%s__isnull' % self.field_path
    132228        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133229        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134230
    135     def title(self):
    136         return self.field.verbose_name
    137 
    138     def choices(self, cl):
     231    def used_params(self):
     232        return [self.lookup_kwarg, self.lookup_kwarg2]
     233   
     234    def _choices(self, cl):
    139235        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    140236            yield {'selected': self.lookup_val == v and not self.lookup_val2,
    141237                   'query_string': cl.get_query_string(
    class BooleanFieldFilterSpec(FilterSpec): 
    149245                                   [self.lookup_kwarg]),
    150246                   'display': _('Unknown')}
    151247
    152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
     248FieldListFilter.register(lambda f: isinstance(f, models.BooleanField)
    153249                              or isinstance(f, models.NullBooleanField),
    154                                  BooleanFieldFilterSpec)
     250                                 BooleanFieldListFilter)
    155251
    156 class ChoicesFilterSpec(FilterSpec):
     252class ChoicesFieldListFilter(FieldListFilter):
    157253    def __init__(self, f, request, params, model, model_admin,
    158                  field_path=None):
    159         super(ChoicesFilterSpec, self).__init__(f, request, params, model,
     254                 field_path):
     255        super(ChoicesFieldListFilter, self).__init__(f, request, params, model,
    160256                                                model_admin,
    161                                                 field_path=field_path)
     257                                                field_path)
    162258        self.lookup_kwarg = '%s__exact' % self.field_path
    163         self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     259        self.lookup_val = request.GET.get(self.lookup_kwarg)
    164260
    165     def choices(self, cl):
     261    def used_params(self):
     262        return [self.lookup_kwarg]
     263       
     264    def _choices(self, cl):
    166265        yield {'selected': self.lookup_val is None,
    167266               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
    168267               'display': _('All')}
    class ChoicesFilterSpec(FilterSpec): 
    172271                                    {self.lookup_kwarg: k}),
    173272                    'display': v}
    174273
    175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     274FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
    176275
    177 class DateFieldFilterSpec(FilterSpec):
     276class DateFieldListFilter(FieldListFilter):
    178277    def __init__(self, f, request, params, model, model_admin,
    179                  field_path=None):
    180         super(DateFieldFilterSpec, self).__init__(f, request, params, model,
     278                 field_path):
     279        super(DateFieldListFilter, self).__init__(f, request, params, model,
    181280                                                  model_admin,
    182                                                   field_path=field_path)
     281                                                  field_path)
    183282
    184283        self.field_generic = '%s__' % self.field_path
    185284
    class DateFieldFilterSpec(FilterSpec): 
    192291                    and today.strftime('%Y-%m-%d 23:59:59') \
    193292                    or today.strftime('%Y-%m-%d')
    194293
     294        self.lookup_kwarg_year = '%s__year' % self.field_path
     295        self.lookup_kwarg_month = '%s__month' % self.field_path
     296        self.lookup_kwarg_day = '%s__day' % self.field_path
     297        self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
     298        self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
     299       
    195300        self.links = (
    196301            (_('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:
     302            (_('Today'), {self.lookup_kwarg_year: str(today.year),
     303                          self.lookup_kwarg_month: str(today.month),
     304                          self.lookup_kwarg_day: str(today.day)}),
     305            (_('Past 7 days'), {self.lookup_kwarg_past_7_days_gte:
    201306                                    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)})
     307                                self.lookup_kwarg_past_7_days_lte:
     308                                    today_str}),
     309            (_('This month'), {self.lookup_kwarg_year: str(today.year),
     310                               self.lookup_kwarg_month: str(today.month)}),
     311            (_('This year'), {self.lookup_kwarg_year: str(today.year)})
    206312        )
    207313
    208     def title(self):
    209         return self.field.verbose_name
    210 
    211     def choices(self, cl):
     314    def used_params(self):
     315        return [self.lookup_kwarg_year, self.lookup_kwarg_month,
     316                self.lookup_kwarg_day, self.lookup_kwarg_past_7_days_gte,
     317                self.lookup_kwarg_past_7_days_lte]
     318   
     319    def get_query_set(self, request, queryset):
     320        """
     321            Override the default behaviour since there can be multiple query
     322            string parameters used for the same date filter (e.g. year + month).
     323        """
     324        query_dict = {}
     325        for p in self.used_params():
     326            if p in self.params:
     327                query_dict[p] = self.params[p]
     328        if len(query_dict):
     329            return queryset.filter(**query_dict)
     330   
     331    def _choices(self, cl):
    212332        for title, param_dict in self.links:
    213333            yield {'selected': self.date_params == param_dict,
    214334                   'query_string': cl.get_query_string(
    class DateFieldFilterSpec(FilterSpec): 
    216336                                   [self.field_generic]),
    217337                   'display': title}
    218338
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     339FieldListFilter.register(lambda f: isinstance(f, models.DateField),
     340                              DateFieldListFilter)
    221341
    222342
    223343# 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):
     344# if a field is eligible to use the BooleanFieldListFilter, that'd be much
     345# more appropriate, and the AllValuesFieldListFilter won't get used for it.
     346class AllValuesFieldListFilter(FieldListFilter):
    227347    def __init__(self, f, request, params, model, model_admin,
    228                  field_path=None):
    229         super(AllValuesFilterSpec, self).__init__(f, request, params, model,
     348                 field_path):
     349        super(AllValuesFieldListFilter, self).__init__(f, request, params, model,
    230350                                                  model_admin,
    231                                                   field_path=field_path)
     351                                                  field_path)
    232352        self.lookup_kwarg = self.field_path
    233353        self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
    234354        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    class AllValuesFilterSpec(FilterSpec): 
    245365        self.lookup_choices = \
    246366            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
    247367
    248     def title(self):
    249         return self.field.verbose_name
    250 
    251     def choices(self, cl):
     368    def used_params(self):
     369        return [self.lookup_kwarg, self.lookup_kwarg_isnull]
     370       
     371    def _choices(self, cl):
    252372        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    253373        yield {'selected': self.lookup_val is None
    254374                           and self.lookup_val_isnull is None,
    class AllValuesFilterSpec(FilterSpec): 
    276396                                    [self.lookup_kwarg]),
    277397                    'display': EMPTY_CHANGELIST_VALUE}
    278398
    279 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     399FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index fbda8b7..cd91273 100644
    a b class ModelAdmin(BaseModelAdmin): 
    10751075        if (actions and request.method == 'POST' and
    10761076                'index' in request.POST and '_save' not in request.POST):
    10771077            if selected:
    1078                 response = self.response_action(request, queryset=cl.get_query_set())
     1078                response = self.response_action(request, queryset=cl.get_query_set(request))
    10791079                if response:
    10801080                    return response
    10811081                else:
    class ModelAdmin(BaseModelAdmin): 
    10911091                helpers.ACTION_CHECKBOX_NAME in request.POST and
    10921092                'index' not in request.POST and '_save' not in request.POST):
    10931093            if selected:
    1094                 response = self.response_action(request, queryset=cl.get_query_set())
     1094                response = self.response_action(request, queryset=cl.get_query_set(request))
    10951095                if response:
    10961096                    return response
    10971097                else:
  • 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..4919eda 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._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..40d14f6 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 for specifying a filter:
     60            #   1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
     61            #   2: ('field', SomeFieldListFilter) - a field-based list filter class
     62            #   3: SomeListFilter - a non-field list filter class
     63            if callable(item) and not isinstance(item, models.Field):
     64                # If item is option 3, it should be a ListFilter.
     65                if not issubclass(item, ListFilter):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not 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..d854703 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): 
    6161            self.list_editable = list_editable
    6262        self.order_field, self.order_type = self.get_ordering()
    6363        self.query = request.GET.get(SEARCH_VAR, '')
    64         self.query_set = self.get_query_set()
     64        self.query_set = self.get_query_set(request)
    6565        self.get_results(request)
    6666        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)
    6867        self.pk_attname = self.lookup_opts.pk.attname
    6968
    7069    def get_filters(self, request):
    7170        filter_specs = []
    7271        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)
     72            for item in self.list_filter:
     73                if callable(item):
     74                    # This is simply a custom ListFilter class.
     75                    spec = item(request, self.cleaned_params, self.model, self.model_admin)
     76                else:
     77                    field_path = None
     78                    try:
     79                        # This is custom FieldListFilter class for a given field.
     80                        field, field_list_filter_class = item
     81                    except (TypeError, ValueError):
     82                        # This is simply a field name, so use the default
     83                        # FieldListFilter class that has been registered for
     84                        # the type of the given field.
     85                        field, field_list_filter_class = item, FieldListFilter.create
     86                    if not isinstance(field, models.Field):
     87                        field_path = field
     88                        field = get_fields_from_path(self.model, field_path)[-1]
     89                    spec = field_list_filter_class(field, request, self.cleaned_params, self.model,
     90                            self.model_admin, field_path=field_path)
    7891                if spec and spec.has_output():
    7992                    filter_specs.append(spec)
    8093        return filter_specs, bool(filter_specs)
    class ChangeList(object): 
    165178        if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
    166179            order_type = params[ORDER_TYPE_VAR]
    167180        return order_field, order_type
    168 
    169     def get_query_set(self):
     181   
     182    def apply_list_filters(self, request, qs, lookup_params):
     183        self.filter_specs, self.has_filters = self.get_filters(request)
     184        for filter_spec in self.filter_specs:
     185            if filter_spec.should_be_used():
     186                qs = filter_spec.get_query_set(request, qs)
     187                for param in filter_spec.used_params():
     188                    try:
     189                        del lookup_params[param]
     190                    except KeyError:
     191                        pass
     192        return qs
     193   
     194    def get_query_set(self, request):
    170195        use_distinct = False
    171196
    172197        qs = self.root_query_set
    class ChangeList(object): 
    187212                field_name = key.split('__', 1)[0]
    188213                try:
    189214                    f = self.lookup_opts.get_field_by_name(field_name)[0]
     215                    if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
     216                        use_distinct = True
    190217                except models.FieldDoesNotExist:
    191                     raise IncorrectLookupParameters
    192                 if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
    193                     use_distinct = True
     218                    # It might be for a non-field custom filter specs.
     219                    pass
    194220
    195221            # if key ends with __in, split parameter into separate values
    196222            if key.endswith('__in'):
    class ChangeList(object): 
    209235                raise SuspiciousOperation(
    210236                    "Filtering by %s not allowed" % key
    211237                )
    212 
    213         # Apply lookup parameters from the query string.
     238       
     239        # Keep a copy of cleaned querystring values so it can be passed to
     240        # the list filters.
     241        self.cleaned_params = lookup_params.copy()
     242       
     243        # Let every list filter modify the qs and params to its liking
     244        qs = self.apply_list_filters(request, qs, lookup_params)
     245       
     246        # Apply the remaining lookup parameters from the query string (i.e.
     247        # those that haven't already been processed by the filters).
    214248        try:
    215249            qs = qs.filter(**lookup_params)
    216250        # 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 415e1fe..47b987a 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.ListFilter`,
     547          where you need to provide a few attributes and override a few
     548          methods::
     549       
     550            from django.contrib.admin import ListFilter
     551            from django.db.models import Q
     552           
     553            class DecadeBornListFilter(ListFilter):
     554                # Human-readable title which will be displayed in the
     555                # right sidebar just above the filter options.
     556                title = u'decade born'
     557               
     558                # This is the code name for the filter that will be used in
     559                # the url query. Providing this attribute is optional. If it is
     560                # not provided then a slugified version of the title will
     561                # automatically be used instead (that is, 'decade-born' in this example).
     562                query_parameter_name = u'decade'
     563               
     564                def get_choices(self, request):
     565                    """
     566                       Returns a list of tuples. The first element in each tuple
     567                       is the coded value for the option that will appear in the
     568                       url query. The second element is the human-readable name
     569                       for the option that will appear in the right sidebar. You
     570                       may specify as many choices as you like, and you may even
     571                       vary the list of choices depending on the HttpRequest
     572                       object provided as argument to this method.
     573                    """
     574                    return (
     575                        (u'80s', u'in the eighties'),
     576                        (u'other', u'other'),
     577                    )
     578               
     579                def get_query_set(self, request, queryset):
     580                    """
     581                       Returns the filtered queryset based on the value provided
     582                       in the query string and retrievable via `get_value()`.
     583                       This method is only called when necessary, that is, if
     584                       the corresponding parameter is present in the query string.
     585                       The HttpRequest object is also provided for your convenience
     586                       in case you need to modify the list of results for each request.
     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        * a tuple, where the first element is a field name and the second
     602          element is a class inheriting from
     603          :mod:`django.contrib.admin.FieldListFilter`. Note that the
     604          `FieldListFilter` API is currently considered internal and prone to
     605          refactoring.
     606       
     607    Finally, the following example, taken from the ``django.contrib.auth.models.User``
     608    model, shows how both ``list_display`` and ``list_filter`` work::
    535609
    536610        class UserAdmin(admin.ModelAdmin):
    537611            list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
    subclass:: 
    543617
    544618    (This example also has ``search_fields`` defined. See below.)
    545619
    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 
    553620.. attribute:: ModelAdmin.list_per_page
    554621
    555622    Set ``list_per_page`` to control how many items appear on each paginated
  • tests/regressiontests/admin_filterspecs/models.py

    diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
    index 5b284c7..db9912f 100644
    a b class Book(models.Model): 
    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..e9c68dd 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 (ListFilter,
     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(ListFilter):
     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, request, 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_name = 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(request)
     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(request)[0][4]
     87        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     88        choice = select_by(filterspec._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(request)
     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(request)[0][4]
     109        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     110        choice = select_by(filterspec._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(request)
     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(request)[0][4]
     129        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     130        choice = select_by(filterspec._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(request)
     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(request)[0][4]
     145        self.assertEqual(force_unicode(filterspec.title), u'date_registered')
     146        choice = select_by(filterspec._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
    48         queryset = changelist.get_query_set()
     158        # Make sure the correct queryset is returned
     159        queryset = changelist.get_query_set(request)
     160        self.assertEqual(list(queryset), [self.django_book])
    49161
    50162        # Make sure the last choice is None and is selected
    51163        filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
    53         choices = list(filterspec.choices(changelist))
     164        self.assertEqual(force_unicode(filterspec.title), u'year')
     165        choices = list(filterspec._choices(changelist))
    54166        self.assertEqual(choices[-1]['selected'], True)
    55167        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    56168
    class FilterSpecsTests(TestCase): 
    59171
    60172        # Make sure the correct choice is selected
    61173        filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
    63         choices = list(filterspec.choices(changelist))
     174        self.assertEqual(force_unicode(filterspec.title), u'year')
     175        choices = list(filterspec._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
    76         queryset = changelist.get_query_set()
     185        # Make sure the correct queryset is returned
     186        queryset = changelist.get_query_set(request)
     187        self.assertEqual(list(queryset), [self.gipsy_book])
    77188
    78189        # Make sure the last choice is None and is selected
    79190        filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
    81         choices = list(filterspec.choices(changelist))
     191        self.assertEqual(force_unicode(filterspec.title), u'author')
     192        choices = list(filterspec._choices(changelist))
    82193        self.assertEqual(choices[-1]['selected'], True)
    83194        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    84195
    class FilterSpecsTests(TestCase): 
    87198
    88199        # Make sure the correct choice is selected
    89200        filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     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._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
    103         queryset = changelist.get_query_set()
     213        # Make sure the correct queryset is returned
     214        queryset = changelist.get_query_set(request)
     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
    106218        filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
    108         choices = list(filterspec.choices(changelist))
     219        self.assertEqual(force_unicode(filterspec.title), u'user')
     220        choices = list(filterspec._choices(changelist))
    109221        self.assertEqual(choices[-1]['selected'], True)
    110222        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    111223
    class FilterSpecsTests(TestCase): 
    114226
    115227        # Make sure the correct choice is selected
    116228        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")
     229        self.assertEqual(force_unicode(filterspec.title), u'user')
     230        choice = select_by(filterspec._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
    131         queryset = changelist.get_query_set()
     241        # Make sure the correct queryset is returned
     242        queryset = changelist.get_query_set(request)
     243        self.assertEqual(list(queryset), [self.lisa])
    132244
    133245        # Make sure the last choice is None and is selected
    134246        filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
    136         choices = list(filterspec.choices(changelist))
     247        self.assertEqual(force_unicode(filterspec.title), u'book')
     248        choices = list(filterspec._choices(changelist))
    137249        self.assertEqual(choices[-1]['selected'], True)
    138250        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    139251
    class FilterSpecsTests(TestCase): 
    142254
    143255        # Make sure the correct choice is selected
    144256        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)
     257        self.assertEqual(force_unicode(filterspec.title), u'book')
     258        choice = select_by(filterspec._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
    155         queryset = changelist.get_query_set()
     266        # Make sure the correct queryset is returned
     267        queryset = changelist.get_query_set(request)
     268        self.assertEqual(list(queryset), [self.alfred])
    156269
    157270        # Make sure the last choice is None and is selected
    158271        filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
    160         choices = list(filterspec.choices(changelist))
     272        self.assertEqual(force_unicode(filterspec.title), u'book')
     273        choices = list(filterspec._choices(changelist))
    161274        self.assertEqual(choices[-1]['selected'], True)
    162275        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    163276
    class FilterSpecsTests(TestCase): 
    166279
    167280        # Make sure the correct choice is selected
    168281        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)
     282        self.assertEqual(force_unicode(filterspec.title), u'book')
     283        choice = select_by(filterspec._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
    183         queryset = changelist.get_query_set()
     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
     303        queryset = changelist.get_query_set(request)
     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(request)[0][3]
     308        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     309        choice = select_by(filterspec._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(request)
     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(request)[0][3]
     322        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     323        choice = select_by(filterspec._choices(changelist), "display", "Yes")
     324        self.assertEqual(choice['selected'], True)
     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(request)
     332        self.assertEqual(list(queryset), [self.django_book])
     333       
     334        # Make sure the correct choice is selected
     335        filterspec = changelist.get_filters(request)[0][3]
     336        self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
     337        choice = select_by(filterspec._choices(changelist), "display", "Unknown")
    200338        self.assertEqual(choice['selected'], True)
    201         self.assertEqual(choice['query_string'], '?completed__exact=1')
     339        self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
     340       
     341    def test_ListFilter(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(request)
     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(request)[0][1]
     355        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     356        choices = list(filterspec._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(request)[0][2]
     368        self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
     369        choices = list(filterspec._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)
    202378
     379        # Make sure the correct queryset is returned
     380        queryset = changelist.get_query_set(request)
     381        self.assertEqual(list(queryset), [self.bio_book])
     382
     383        # Make sure the correct choice is selected
     384        filterspec = changelist.get_filters(request)[0][1]
     385        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     386        choices = list(filterspec._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)
     395
     396        # Make sure the correct queryset is returned
     397        queryset = changelist.get_query_set(request)
     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(request)[0][1]
     402        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     403        choices = list(filterspec._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(request)
     415        self.assertEqual(list(queryset), [self.djangonaut_book])
     416
     417        # Make sure the correct choices are selected
     418        filterspec = changelist.get_filters(request)[0][1]
     419        self.assertEqual(force_unicode(filterspec.title), u'publication decade')
     420        choices = list(filterspec._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(request)[0][0]
     426        self.assertEqual(force_unicode(filterspec.title), u'author')
     427        choice = select_by(filterspec._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/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    index b65f8a4..cfa4123 100644
    a b class Gadget(models.Model): 
    566566        return self.name
    567567
    568568class CustomChangeList(ChangeList):
    569     def get_query_set(self):
     569    def get_query_set(self, request):
    570570        return self.root_query_set.filter(pk=9999) # Does not exist
    571571
    572572class GadgetAdmin(admin.ModelAdmin):
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..c14f743 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 (ListFilter,
     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(ListFilter):
     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