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

File 5833.custom-filterspecs.2.diff, 34.6 KB (added by Julien Phalip, 8 years ago)

Introducing SimpleFilterSpecs?, with tests.

  • django/contrib/admin/__init__.py

    diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
    index f8e634e..77c1143 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 FilterSpec, FieldFilterSpec
    34from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
    45from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    56from 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..12322e0 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
    1819class FilterSpec(object):
    19     filter_specs = []
    20     def __init__(self, f, request, params, model, model_admin,
    21                  field_path=None):
    22         self.field = f
     20    _filter_specs = []
     21    _high_priority_index = 0
     22   
     23    def __init__(self, request, params, model, model_admin):
    2324        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)
    4125
    4226    def has_output(self):
    4327        return True
    4428
    45     def choices(self, cl):
    46         raise NotImplementedError()
    47 
    48     def title(self):
    49         return self.field.verbose_name
     29    def _choices(self, cl):
     30        raise NotImplementedError
     31
     32    def get_title(self):
     33        raise NotImplementedError
     34
     35    def get_query_set(self, cl, qs):
     36        return False
     37   
     38    def _consumed_params(self):
     39        """
     40        Return a list of parameters to consume from the change list
     41        querystring.
     42       
     43        Override this for non-field based FilterSpecs subclasses in order
     44        to consume custom GET parameters, as any GET parameters that are not
     45        consumed and are not a field name raises an exception.
     46        """
     47        return []
    5048
    5149    def output(self, cl):
    5250        t = []
    5351        if self.has_output():
    54             t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
     52            t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.get_title()))
    5553
    56             for choice in self.choices(cl):
     54            for choice in self._choices(cl):
    5755                t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
    5856                    ((choice['selected'] and ' class="selected"' or ''),
    5957                     iri_to_uri(choice['query_string']),
    6058                     choice['display']))
    6159            t.append('</ul>\n\n')
    6260        return mark_safe("".join(t))
     61   
     62    @classmethod
     63    def register(cls, test, factory, high_priority=True):
     64        if high_priority:
     65            cls._filter_specs.insert(cls._high_priority_index, (test, factory))
     66            cls._high_priority_index += 1
     67        else:
     68            cls._filter_specs.append((test, factory))
     69
     70           
     71           
     72class SimpleFilterSpec(FilterSpec):
     73   
     74    def __init__(self, request, params, model, model_admin):
     75        super(SimpleFilterSpec, self).__init__(request, params, model, model_admin)
     76        self.lookup_kwarg = self.get_lookup_parameter()
     77        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
     78        self.lookup_choices = self.get_choices(request)
     79   
     80    def has_output(self):
     81        return len(self.lookup_choices) > 0
     82       
     83    def get_lookup_parameter(self):
     84        """
     85            Returns the parameter that should be used in the query string
     86            for that filter. Defaults to the title, slugified.
     87        """
     88        return slugify(self.get_title())
     89   
     90    def get_value(self):
     91        """
     92            Returns the value given in the query string for this filter,
     93            if any. Returns None otherwise.
     94        """
     95        return self.params.get(self.lookup_kwarg, None)
     96
     97    def get_choices(self, request):
     98        """
     99            Must be overriden to return a list of tuples (value, verbose value)
     100        """
     101        raise NotImplementedError
     102
     103    def _consumed_params(self):
     104        return [self.lookup_kwarg]
     105
     106    def _choices(self, cl):
     107        yield {'selected': self.lookup_val is None,
     108               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
     109               'display': _('All')}
     110        for k, v in self.lookup_choices:
     111            yield {'selected': self.lookup_val == k,
     112                   'query_string': cl.get_query_string(
     113                                   {self.lookup_kwarg: k},
     114                                   []),
     115                   'display': v}
    63116
    64 class RelatedFilterSpec(FilterSpec):
    65     def __init__(self, f, request, params, model, model_admin,
     117
     118           
     119class FieldFilterSpec(FilterSpec):
     120
     121    def __init__(self, field, request, params, model, model_admin, \
     122                 field_path=None):
     123        super(FieldFilterSpec, self).__init__(request, params, model, \
     124                                              model_admin)
     125        self.field = field
     126        if field_path is None:
     127            if isinstance(field, models.related.RelatedObject):
     128                self.field_path = field.var_name
     129            else:
     130                self.field_path = field.name
     131        else:
     132            self.field_path = field_path
     133
     134    def get_title(self):
     135        return self.field.verbose_name
     136
     137    @classmethod
     138    def create(cls, field, request, params, model, model_admin, field_path=None):
     139        for test, factory in cls._filter_specs:
     140            if test(field):
     141                return factory(field, request, params, model, model_admin,
     142                               field_path=field_path)
     143
     144       
     145class RelatedFilterSpec(FieldFilterSpec):
     146    def __init__(self, field, request, params, model, model_admin,
    66147                 field_path=None):
    67148        super(RelatedFilterSpec, self).__init__(
    68             f, request, params, model, model_admin, field_path=field_path)
     149            field, request, params, model, model_admin, field_path=field_path)
    69150
    70         other_model = get_model_from_relation(f)
    71         if isinstance(f, (models.ManyToManyField,
     151        other_model = get_model_from_relation(field)
     152        if isinstance(field, (models.ManyToManyField,
    72153                          models.related.RelatedObject)):
    73154            # no direct field on this model, get name from other model
    74155            self.lookup_title = other_model._meta.verbose_name
    75156        else:
    76             self.lookup_title = f.verbose_name # use field name
     157            self.lookup_title = field.verbose_name # use field name
    77158        rel_name = other_model._meta.pk.name
    78159        self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
    79160        self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
    80161        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    81162        self.lookup_val_isnull = request.GET.get(
    82163                                      self.lookup_kwarg_isnull, None)
    83         self.lookup_choices = f.get_choices(include_blank=False)
     164        self.lookup_choices = field.get_choices(include_blank=False)
    84165
    85166    def has_output(self):
    86167        if isinstance(self.field, models.related.RelatedObject) \
    class RelatedFilterSpec(FilterSpec): 
    91172            extra = 0
    92173        return len(self.lookup_choices) + extra > 1
    93174
    94     def title(self):
     175    def get_title(self):
    95176        return self.lookup_title
    96177
    97     def choices(self, cl):
     178    def _choices(self, cl):
    98179        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    99180        yield {'selected': self.lookup_val is None
    100181                           and not self.lookup_val_isnull,
    class RelatedFilterSpec(FilterSpec): 
    117198                                   [self.lookup_kwarg]),
    118199                   'display': EMPTY_CHANGELIST_VALUE}
    119200
    120 FilterSpec.register(lambda f: (
     201FieldFilterSpec.register(lambda f: (
    121202        hasattr(f, 'rel') and bool(f.rel) or
    122         isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
     203        isinstance(f, models.related.RelatedObject)), RelatedFilterSpec, False)
    123204
    124 class BooleanFieldFilterSpec(FilterSpec):
     205class BooleanFieldFilterSpec(FieldFilterSpec):
    125206    def __init__(self, f, request, params, model, model_admin,
    126207                 field_path=None):
    127208        super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
    class BooleanFieldFilterSpec(FilterSpec): 
    132213        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    133214        self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
    134215
    135     def title(self):
    136         return self.field.verbose_name
    137 
    138     def choices(self, cl):
     216    def _choices(self, cl):
    139217        for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
    140218            yield {'selected': self.lookup_val == v and not self.lookup_val2,
    141219                   'query_string': cl.get_query_string(
    class BooleanFieldFilterSpec(FilterSpec): 
    149227                                   [self.lookup_kwarg]),
    150228                   'display': _('Unknown')}
    151229
    152 FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
     230FieldFilterSpec.register(lambda f: isinstance(f, models.BooleanField)
    153231                              or isinstance(f, models.NullBooleanField),
    154                                  BooleanFieldFilterSpec)
     232                                 BooleanFieldFilterSpec, False)
    155233
    156 class ChoicesFilterSpec(FilterSpec):
     234class ChoicesFilterSpec(FieldFilterSpec):
    157235    def __init__(self, f, request, params, model, model_admin,
    158236                 field_path=None):
    159237        super(ChoicesFilterSpec, self).__init__(f, request, params, model,
    class ChoicesFilterSpec(FilterSpec): 
    162240        self.lookup_kwarg = '%s__exact' % self.field_path
    163241        self.lookup_val = request.GET.get(self.lookup_kwarg, None)
    164242
    165     def choices(self, cl):
     243    def _choices(self, cl):
    166244        yield {'selected': self.lookup_val is None,
    167245               'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
    168246               'display': _('All')}
    class ChoicesFilterSpec(FilterSpec): 
    172250                                    {self.lookup_kwarg: k}),
    173251                    'display': v}
    174252
    175 FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
     253FieldFilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec, False)
    176254
    177 class DateFieldFilterSpec(FilterSpec):
     255class DateFieldFilterSpec(FieldFilterSpec):
    178256    def __init__(self, f, request, params, model, model_admin,
    179257                 field_path=None):
    180258        super(DateFieldFilterSpec, self).__init__(f, request, params, model,
    class DateFieldFilterSpec(FilterSpec): 
    205283            (_('This year'), {'%s__year' % self.field_path: str(today.year)})
    206284        )
    207285
    208     def title(self):
    209         return self.field.verbose_name
    210 
    211     def choices(self, cl):
     286    def _choices(self, cl):
    212287        for title, param_dict in self.links:
    213288            yield {'selected': self.date_params == param_dict,
    214289                   'query_string': cl.get_query_string(
    class DateFieldFilterSpec(FilterSpec): 
    216291                                   [self.field_generic]),
    217292                   'display': title}
    218293
    219 FilterSpec.register(lambda f: isinstance(f, models.DateField),
    220                               DateFieldFilterSpec)
     294FieldFilterSpec.register(lambda f: isinstance(f, models.DateField),
     295                              DateFieldFilterSpec, False)
    221296
    222297
    223298# This should be registered last, because it's a last resort. For example,
    224299# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
    225300# more appropriate, and the AllValuesFilterSpec won't get used for it.
    226 class AllValuesFilterSpec(FilterSpec):
     301class AllValuesFilterSpec(FieldFilterSpec):
    227302    def __init__(self, f, request, params, model, model_admin,
    228303                 field_path=None):
    229304        super(AllValuesFilterSpec, self).__init__(f, request, params, model,
    class AllValuesFilterSpec(FilterSpec): 
    245320        self.lookup_choices = \
    246321            queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
    247322
    248     def title(self):
    249         return self.field.verbose_name
    250 
    251     def choices(self, cl):
     323    def _choices(self, cl):
    252324        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    253325        yield {'selected': self.lookup_val is None
    254326                           and self.lookup_val_isnull is None,
    class AllValuesFilterSpec(FilterSpec): 
    276348                                    [self.lookup_kwarg]),
    277349                    'display': EMPTY_CHANGELIST_VALUE}
    278350
    279 FilterSpec.register(lambda f: True, AllValuesFilterSpec)
     351FieldFilterSpec.register(lambda f: True, AllValuesFilterSpec, False)
  • 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..4fdb8a1 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 FilterSpec, FieldFilterSpec
    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 simple field filter, poss. w/ relationships (eg, 'field__rel')
     61            #   2: ('field', SomeFieldFilterSpec) - a field-based filter spec
     62            #   3: SomeFilterSpec - a non-field filter spec
     63            if callable(item) and not isinstance(item, models.Field):
     64                # If item is option 3, it should be a FilterSpec, but not a FieldFilterSpec
     65                if not issubclass(item, FilterSpec) or issubclass(item, FieldFilterSpec):
     66                    raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
     67                            " which is not of type FilterSpec."
     68                            % (cls.__name__, idx, item))
     69            else:
     70                try:
     71                    # Check for option #2 (tuple)
     72                    field, factory = item
     73                except (TypeError, ValueError):
     74                    # item is option #1
     75                    field = item
     76                else:
     77                    # item is option #2
     78                    if not issubclass(factory, FieldFilterSpec):
     79                        raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
     80                            " refers to '%s' which is not of type FieldFilterSpec."
     81                            % (cls.__name__, idx, factory.__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..77a52d8 100644
    a b  
    1 from django.contrib.admin.filterspecs import FilterSpec
     1from django.contrib.admin.filterspecs import FilterSpec, FieldFilterSpec
    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 FilterSpec class.
     76                    spec = item(request, self.params, self.model, self.model_admin)
     77                else:
     78                    field_path = None
     79                    try:
     80                        # This is custom FilterSpec class for a given field.
     81                        field, factory = item
     82                    except (TypeError, ValueError):
     83                        # This simply a field name, so use the default
     84                        # 'create' factory.
     85                        field, factory = item, FieldFilterSpec.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 = factory(field, request, self.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): 
    166179            order_type = params[ORDER_TYPE_VAR]
    167180        return order_field, order_type
    168181
     182    def apply_filter_specs(self, qs, lookup_params):
     183        for filter_spec in self.filter_specs:
     184            new_qs = filter_spec.get_query_set(self, qs)
     185            if new_qs is not None and new_qs is not False:
     186                qs = new_qs
     187                # Only consume params if we got a new queryset
     188                for param in filter_spec._consumed_params():
     189                    try:
     190                        del lookup_params[param]
     191                    except KeyError:
     192                        pass
     193        return qs
     194   
    169195    def get_query_set(self):
    170196        use_distinct = False
    171197
    class ChangeList(object): 
    187213                field_name = key.split('__', 1)[0]
    188214                try:
    189215                    f = self.lookup_opts.get_field_by_name(field_name)[0]
     216                    if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
     217                        use_distinct = True
    190218                except models.FieldDoesNotExist:
    191                     raise IncorrectLookupParameters
    192                 if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
    193                     use_distinct = True
     219                    # It might be for a non-field custom filter specs.
     220                    pass
    194221
    195222            # if key ends with __in, split parameter into separate values
    196223            if key.endswith('__in'):
    class ChangeList(object): 
    209236                raise SuspiciousOperation(
    210237                    "Filtering by %s not allowed" % key
    211238                )
    212 
     239        # Let every filter spec modify the qs and params to its liking
     240        qs = self.apply_filter_specs(qs, lookup_params)
     241       
    213242        # Apply lookup parameters from the query string.
    214243        try:
    215244            qs = qs.filter(**lookup_params)
  • tests/regressiontests/admin_filterspecs/tests.py

    diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
    index 8b9e734..a39db9b 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 SimpleFilterSpec
    89
    910from models import Book, BoolTest
    1011
    1112def select_by(dictlist, key, value):
    1213    return [x for x in dictlist if x[key] == value][0]
    1314
     15
     16
     17class DecadeFilterSpec(SimpleFilterSpec):
     18   
     19    def get_title(self):
     20        return u'publication decade'
     21   
     22    def get_choices(self, request):
     23        return (
     24            (u'90s', u'1990\'s'),
     25            (u'00s', u'2000\'s'),
     26            (u'other', u'other'),
     27        )
     28   
     29    def get_query_set(self, cl, qs):
     30        decade = self.get_value()
     31        if decade == u'90s':
     32            return qs.filter(year__gte=1990, year__lte=1999)
     33        if decade == u'00s':
     34            return qs.filter(year__gte=2000, year__lte=2009)
     35        return qs
     36
     37
    1438class FilterSpecsTests(TestCase):
    1539
    1640    def setUp(self):
    1741        # Users
    1842        self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
    1943        self.bob = User.objects.create_user('bob', 'bob@example.com')
    20         lisa = User.objects.create_user('lisa', 'lisa@example.com')
     44        self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
    2145
    2246        #Books
     47        self.djangonaut_book = Book.objects.create(title='Djangonaut: an art of living', year=2009, author=self.alfred)
    2348        self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred)
    2449        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()
     50        self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
     51        self.gipsy_book.contributors = [self.bob, self.lisa]
     52        self.gipsy_book.save()
    2853
    2954        # BoolTests
    3055        self.trueTest = BoolTest.objects.create(completed=True)
    class FilterSpecsTests(TestCase): 
    4974
    5075        # Make sure the last choice is None and is selected
    5176        filterspec = changelist.get_filters(request)[0][0]
    52         self.assertEqual(force_unicode(filterspec.title()), u'year')
    53         choices = list(filterspec.choices(changelist))
     77        self.assertEqual(force_unicode(filterspec.get_title()), u'year')
     78        choices = list(filterspec._choices(changelist))
    5479        self.assertEqual(choices[-1]['selected'], True)
    5580        self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
    5681
    class FilterSpecsTests(TestCase): 
    5984
    6085        # Make sure the correct choice is selected
    6186        filterspec = changelist.get_filters(request)[0][0]
    62         self.assertEqual(force_unicode(filterspec.title()), u'year')
    63         choices = list(filterspec.choices(changelist))
     87        self.assertEqual(force_unicode(filterspec.get_title()), u'year')
     88        choices = list(filterspec._choices(changelist))
    6489        self.assertEqual(choices[2]['selected'], True)
    6590        self.assertEqual(choices[2]['query_string'], '?year=2002')
    6691
    class FilterSpecsTests(TestCase): 
    77102
    78103        # Make sure the last choice is None and is selected
    79104        filterspec = changelist.get_filters(request)[0][1]
    80         self.assertEqual(force_unicode(filterspec.title()), u'author')
    81         choices = list(filterspec.choices(changelist))
     105        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
     106        choices = list(filterspec._choices(changelist))
    82107        self.assertEqual(choices[-1]['selected'], True)
    83108        self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
    84109
    class FilterSpecsTests(TestCase): 
    87112
    88113        # Make sure the correct choice is selected
    89114        filterspec = changelist.get_filters(request)[0][1]
    90         self.assertEqual(force_unicode(filterspec.title()), u'author')
     115        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
    91116        # order of choices depends on User model, which has no order
    92         choice = select_by(filterspec.choices(changelist), "display", "alfred")
     117        choice = select_by(filterspec._choices(changelist), "display", "alfred")
    93118        self.assertEqual(choice['selected'], True)
    94119        self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
    95120
    class FilterSpecsTests(TestCase): 
    104129
    105130        # Make sure the last choice is None and is selected
    106131        filterspec = changelist.get_filters(request)[0][2]
    107         self.assertEqual(force_unicode(filterspec.title()), u'user')
    108         choices = list(filterspec.choices(changelist))
     132        self.assertEqual(force_unicode(filterspec.get_title()), u'user')
     133        choices = list(filterspec._choices(changelist))
    109134        self.assertEqual(choices[-1]['selected'], True)
    110135        self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
    111136
    class FilterSpecsTests(TestCase): 
    114139
    115140        # Make sure the correct choice is selected
    116141        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")
     142        self.assertEqual(force_unicode(filterspec.get_title()), u'user')
     143        choice = select_by(filterspec._choices(changelist), "display", "bob")
    119144        self.assertEqual(choice['selected'], True)
    120145        self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
    121146
    class FilterSpecsTests(TestCase): 
    132157
    133158        # Make sure the last choice is None and is selected
    134159        filterspec = changelist.get_filters(request)[0][0]
    135         self.assertEqual(force_unicode(filterspec.title()), u'book')
    136         choices = list(filterspec.choices(changelist))
     160        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     161        choices = list(filterspec._choices(changelist))
    137162        self.assertEqual(choices[-1]['selected'], True)
    138163        self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
    139164
    class FilterSpecsTests(TestCase): 
    142167
    143168        # Make sure the correct choice is selected
    144169        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)
     170        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     171        choice = select_by(filterspec._choices(changelist), "display", self.bio_book.title)
    147172        self.assertEqual(choice['selected'], True)
    148173        self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
    149174
    class FilterSpecsTests(TestCase): 
    156181
    157182        # Make sure the last choice is None and is selected
    158183        filterspec = changelist.get_filters(request)[0][1]
    159         self.assertEqual(force_unicode(filterspec.title()), u'book')
    160         choices = list(filterspec.choices(changelist))
     184        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     185        choices = list(filterspec._choices(changelist))
    161186        self.assertEqual(choices[-1]['selected'], True)
    162187        self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
    163188
    class FilterSpecsTests(TestCase): 
    166191
    167192        # Make sure the correct choice is selected
    168193        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)
     194        self.assertEqual(force_unicode(filterspec.get_title()), u'book')
     195        choice = select_by(filterspec._choices(changelist), "display", self.django_book.title)
    171196        self.assertEqual(choice['selected'], True)
    172197        self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
    173198
    class FilterSpecsTests(TestCase): 
    184209
    185210        # Make sure the last choice is None and is selected
    186211        filterspec = changelist.get_filters(request)[0][0]
    187         self.assertEqual(force_unicode(filterspec.title()), u'completed')
    188         choices = list(filterspec.choices(changelist))
     212        self.assertEqual(force_unicode(filterspec.get_title()), u'completed')
     213        choices = list(filterspec._choices(changelist))
    189214        self.assertEqual(choices[-1]['selected'], False)
    190215        self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
    191216
    class FilterSpecsTests(TestCase): 
    194219
    195220        # Make sure the correct choice is selected
    196221        filterspec = changelist.get_filters(request)[0][0]
    197         self.assertEqual(force_unicode(filterspec.title()), u'completed')
     222        self.assertEqual(force_unicode(filterspec.get_title()), u'completed')
    198223        # order of choices depends on User model, which has no order
    199         choice = select_by(filterspec.choices(changelist), "display", "Yes")
     224        choice = select_by(filterspec._choices(changelist), "display", "Yes")
    200225        self.assertEqual(choice['selected'], True)
    201226        self.assertEqual(choice['query_string'], '?completed__exact=1')
    202227
     228    def test_simple_filterspecs(self):
     229        modeladmin = DecadeFilterBookAdmin(Book, admin.site)
     230       
     231        # Look for books in the 1990s --------
     232       
     233        request = self.request_factory.get('/', {'publication-decade': '90s'})
     234        changelist = self.get_changelist(request, Book, modeladmin)
     235
     236        # Make sure the correct queryset is returned
     237        queryset = changelist.get_query_set()
     238        self.assertEqual(list(queryset), [self.bio_book])
     239
     240        # Make sure the correct choice is selected
     241        filterspec = changelist.get_filters(request)[0][1]
     242        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     243        choices = list(filterspec._choices(changelist))
     244        self.assertEqual(choices[1]['selected'], True)
     245        self.assertEqual(choices[1]['query_string'], '?publication-decade=90s')
     246       
     247        # Look for books in the 2000s --------
     248       
     249        request = self.request_factory.get('/', {'publication-decade': '00s'})
     250        changelist = self.get_changelist(request, Book, modeladmin)
     251
     252        # Make sure the correct queryset is returned
     253        queryset = changelist.get_query_set()
     254        self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
     255
     256        # Make sure the correct choice is selected
     257        filterspec = changelist.get_filters(request)[0][1]
     258        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     259        choices = list(filterspec._choices(changelist))
     260        self.assertEqual(choices[2]['selected'], True)
     261        self.assertEqual(choices[2]['query_string'], '?publication-decade=00s')
     262       
     263        # Combine multiple filters --------
     264       
     265        request = self.request_factory.get('/', {'publication-decade': '00s', 'author__id__exact': self.alfred.pk})
     266        changelist = self.get_changelist(request, Book, modeladmin)
     267
     268        # Make sure the correct queryset is returned
     269        queryset = changelist.get_query_set()
     270        self.assertEqual(list(queryset), [self.djangonaut_book])
     271
     272        # Make sure the correct choices are selected
     273        filterspec = changelist.get_filters(request)[0][1]
     274        self.assertEqual(force_unicode(filterspec.get_title()), u'publication decade')
     275        choices = list(filterspec._choices(changelist))
     276        self.assertEqual(choices[2]['selected'], True)
     277        self.assertEqual(choices[2]['query_string'], '?publication-decade=00s&author__id__exact=%s' % self.alfred.pk)
     278       
     279        filterspec = changelist.get_filters(request)[0][0]
     280        self.assertEqual(force_unicode(filterspec.get_title()), u'author')
     281        choice = select_by(filterspec._choices(changelist), "display", "alfred")
     282        self.assertEqual(choice['selected'], True)
     283        self.assertEqual(choice['query_string'], '?publication-decade=00s&author__id__exact=%s' % self.alfred.pk)
     284       
     285       
    203286class CustomUserAdmin(UserAdmin):
    204287    list_filter = ('books_authored', 'books_contributed')
    205288
    class BookAdmin(admin.ModelAdmin): 
    209292
    210293class BoolTestAdmin(admin.ModelAdmin):
    211294    list_filter = ('completed',)
     295
     296class DecadeFilterBookAdmin(admin.ModelAdmin):
     297    list_filter = ('author', DecadeFilterSpec)
     298    order_by = '-id'
Back to Top