diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py
index f8e634e..d725568 100644
--- a/django/contrib/admin/__init__.py
+++ b/django/contrib/admin/__init__.py
@@ -4,7 +4,7 @@ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
from django.contrib.admin.options import StackedInline, TabularInline
from django.contrib.admin.sites import AdminSite, site
-
+from django.contrib.admin.filterspecs import ListFilter, FieldListFilter
def autodiscover():
"""
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index 965b32b..39288f0 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -7,80 +7,176 @@ certain test -- e.g. being a DateField or ForeignKey.
"""
from django.db import models
+from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import smart_unicode, iri_to_uri
from django.utils.translation import ugettext as _
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from django.contrib.admin.util import get_model_from_relation, \
- reverse_field_path, get_limit_choices_to_from_path
+from django.contrib.admin.util import (get_model_from_relation,
+ reverse_field_path, get_limit_choices_to_from_path)
+from django.template.defaultfilters import slugify
import datetime
-class FilterSpec(object):
- filter_specs = []
- def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- self.field = f
+class ListFilterBase(object):
+ title = None # Human-readable title to appear in the right sidebar.
+
+ def __init__(self, request, params, model, model_admin):
+ if self.title is None:
+ raise ImproperlyConfigured("The list filter '%s' does not specify "
+ "a 'title'." % self.__class__.__name__)
self.params = params
- self.field_path = field_path
- if field_path is None:
- if isinstance(f, models.related.RelatedObject):
- self.field_path = f.var_name
- else:
- self.field_path = f.name
-
- def register(cls, test, factory):
- cls.filter_specs.append((test, factory))
- register = classmethod(register)
-
- def create(cls, f, request, params, model, model_admin, field_path=None):
- for test, factory in cls.filter_specs:
- if test(f):
- return factory(f, request, params, model, model_admin,
- field_path=field_path)
- create = classmethod(create)
+ def should_be_used(self):
+ """
+ Returns True if the filter should be used, based on the parameters
+ given in the query string.
+ """
+ for p in self.used_params():
+ if p in self.params:
+ return True
+ return False
+
def has_output(self):
- return True
+ raise NotImplementedError
- def choices(self, cl):
- raise NotImplementedError()
+ def _choices(self, cl):
+ raise NotImplementedError
- def title(self):
- return self.field.verbose_name
+ def get_query_set(self, request, queryset):
+ raise NotImplementedError
+
+ def used_params(self):
+ """
+ Return a list of parameters to consume from the change list
+ querystring.
+ """
+ raise NotImplementedError
def output(self, cl):
t = []
if self.has_output():
- t.append(_(u'
By %s:
\n\n') % escape(self.title()))
+ t.append(_(u'By %s:
\n\n') % escape(self.title))
- for choice in self.choices(cl):
+ for choice in self._choices(cl):
t.append(u'- %s
\n' % \
((choice['selected'] and ' class="selected"' or ''),
iri_to_uri(choice['query_string']),
choice['display']))
t.append('
\n\n')
return mark_safe("".join(t))
+
+
+
+
+
+class ListFilter(ListFilterBase):
+ """
+ API to make the creation of a custom non-field list filter as simple
+ and easy as possible.
+ """
+
+ # Parameter that should be used in the query string for that filter.
+ # Defaults to the title, slugified.
+ query_parameter_name = None
+
+ def __init__(self, request, params, model, model_admin):
+ super(ListFilter, self).__init__(request, params, model, model_admin)
+ if self.query_parameter_name is None:
+ self.query_parameter_name = slugify(self.title)
+ self.lookup_choices = self.get_choices(request)
+
+ def has_output(self):
+ return len(self.lookup_choices) > 0
+
+ def get_value(self):
+ """
+ Returns the value given in the query string for this filter,
+ if any. Returns None otherwise.
+ """
+ return self.params.get(self.query_parameter_name, None)
+
+ def get_choices(self, request):
+ """
+ Must be overriden to return a list of tuples (value, verbose value)
+ """
+ raise NotImplementedError
+
+ def used_params(self):
+ return [self.query_parameter_name]
+
+ def _choices(self, cl):
+ yield {'selected': self.get_value() is None,
+ 'query_string': cl.get_query_string({}, [self.query_parameter_name]),
+ 'display': _('All')}
+ for k, v in self.lookup_choices:
+ yield {'selected': self.get_value() == k,
+ 'query_string': cl.get_query_string(
+ {self.query_parameter_name: k},
+ []),
+ 'display': v}
+
+
+
+class FieldListFilter(ListFilterBase):
+ _field_list_filters = []
+ _take_priority_index = 0
+
+ def __init__(self, field, request, params, model, model_admin, \
+ field_path):
+ self.field = field
+ self.field_path = field_path
+ self.title = field_path
+ super(FieldListFilter, self).__init__(request, params, model, \
+ model_admin)
-class RelatedFilterSpec(FilterSpec):
- def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- super(RelatedFilterSpec, self).__init__(
- f, request, params, model, model_admin, field_path=field_path)
+ def has_output(self):
+ return True
+
+ def get_query_set(self, request, queryset):
+ for p in self.used_params():
+ if p in self.params:
+ return queryset.filter(**{p: self.params[p]})
+
+ @classmethod
+ def register(cls, test, list_filter_class, take_priority=False):
+ if take_priority:
+ # This is to allow overriding the default filters for certain types
+ # of fields with some custom filters. The first found in the list
+ # is used in priority.
+ cls._field_list_filters.insert(cls._take_priority_index, (test, list_filter_class))
+ _take_priority_index += 1
+ else:
+ cls._field_list_filters.append((test, list_filter_class))
+
+ @classmethod
+ def create(cls, field, request, params, model, model_admin, field_path):
+ for test, list_filter_class in cls._field_list_filters:
+ if test(field):
+ return list_filter_class(field, request, params, model, model_admin,
+ field_path=field_path)
- other_model = get_model_from_relation(f)
- if isinstance(f, (models.ManyToManyField,
+
+class RelatedFieldListFilter(FieldListFilter):
+ def __init__(self, field, request, params, model, model_admin,
+ field_path):
+ super(RelatedFieldListFilter, self).__init__(
+ field, request, params, model, model_admin, field_path)
+
+ other_model = get_model_from_relation(field)
+ if isinstance(field, (models.ManyToManyField,
models.related.RelatedObject)):
# no direct field on this model, get name from other model
self.lookup_title = other_model._meta.verbose_name
else:
- self.lookup_title = f.verbose_name # use field name
+ self.lookup_title = field.verbose_name # use field name
rel_name = other_model._meta.pk.name
self.lookup_kwarg = '%s__%s__exact' % (self.field_path, rel_name)
self.lookup_kwarg_isnull = '%s__isnull' % (self.field_path)
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
self.lookup_val_isnull = request.GET.get(
self.lookup_kwarg_isnull, None)
- self.lookup_choices = f.get_choices(include_blank=False)
+ self.lookup_choices = field.get_choices(include_blank=False)
+ self.title = self.lookup_title
def has_output(self):
if isinstance(self.field, models.related.RelatedObject) \
@@ -91,10 +187,10 @@ class RelatedFilterSpec(FilterSpec):
extra = 0
return len(self.lookup_choices) + extra > 1
- def title(self):
- return self.lookup_title
-
- def choices(self, cl):
+ def used_params(self):
+ return [self.lookup_kwarg, self.lookup_kwarg_isnull]
+
+ def _choices(self, cl):
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
yield {'selected': self.lookup_val is None
and not self.lookup_val_isnull,
@@ -117,25 +213,25 @@ class RelatedFilterSpec(FilterSpec):
[self.lookup_kwarg]),
'display': EMPTY_CHANGELIST_VALUE}
-FilterSpec.register(lambda f: (
+FieldListFilter.register(lambda f: (
hasattr(f, 'rel') and bool(f.rel) or
- isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
+ isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
-class BooleanFieldFilterSpec(FilterSpec):
+class BooleanFieldListFilter(FieldListFilter):
def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
+ field_path):
+ super(BooleanFieldListFilter, self).__init__(f, request, params, model,
model_admin,
- field_path=field_path)
+ field_path)
self.lookup_kwarg = '%s__exact' % self.field_path
self.lookup_kwarg2 = '%s__isnull' % self.field_path
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
- def title(self):
- return self.field.verbose_name
-
- def choices(self, cl):
+ def used_params(self):
+ return [self.lookup_kwarg, self.lookup_kwarg2]
+
+ def _choices(self, cl):
for k, v in ((_('All'), None), (_('Yes'), '1'), (_('No'), '0')):
yield {'selected': self.lookup_val == v and not self.lookup_val2,
'query_string': cl.get_query_string(
@@ -149,20 +245,23 @@ class BooleanFieldFilterSpec(FilterSpec):
[self.lookup_kwarg]),
'display': _('Unknown')}
-FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
+FieldListFilter.register(lambda f: isinstance(f, models.BooleanField)
or isinstance(f, models.NullBooleanField),
- BooleanFieldFilterSpec)
+ BooleanFieldListFilter)
-class ChoicesFilterSpec(FilterSpec):
+class ChoicesFieldListFilter(FieldListFilter):
def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- super(ChoicesFilterSpec, self).__init__(f, request, params, model,
+ field_path):
+ super(ChoicesFieldListFilter, self).__init__(f, request, params, model,
model_admin,
- field_path=field_path)
+ field_path)
self.lookup_kwarg = '%s__exact' % self.field_path
- self.lookup_val = request.GET.get(self.lookup_kwarg, None)
+ self.lookup_val = request.GET.get(self.lookup_kwarg)
- def choices(self, cl):
+ def used_params(self):
+ return [self.lookup_kwarg]
+
+ def _choices(self, cl):
yield {'selected': self.lookup_val is None,
'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
'display': _('All')}
@@ -172,14 +271,14 @@ class ChoicesFilterSpec(FilterSpec):
{self.lookup_kwarg: k}),
'display': v}
-FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
+FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
-class DateFieldFilterSpec(FilterSpec):
+class DateFieldListFilter(FieldListFilter):
def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- super(DateFieldFilterSpec, self).__init__(f, request, params, model,
+ field_path):
+ super(DateFieldListFilter, self).__init__(f, request, params, model,
model_admin,
- field_path=field_path)
+ field_path)
self.field_generic = '%s__' % self.field_path
@@ -192,23 +291,44 @@ class DateFieldFilterSpec(FilterSpec):
and today.strftime('%Y-%m-%d 23:59:59') \
or today.strftime('%Y-%m-%d')
+ self.lookup_kwarg_year = '%s__year' % self.field_path
+ self.lookup_kwarg_month = '%s__month' % self.field_path
+ self.lookup_kwarg_day = '%s__day' % self.field_path
+ self.lookup_kwarg_past_7_days_gte = '%s__gte' % self.field_path
+ self.lookup_kwarg_past_7_days_lte = '%s__lte' % self.field_path
+
self.links = (
(_('Any date'), {}),
- (_('Today'), {'%s__year' % self.field_path: str(today.year),
- '%s__month' % self.field_path: str(today.month),
- '%s__day' % self.field_path: str(today.day)}),
- (_('Past 7 days'), {'%s__gte' % self.field_path:
+ (_('Today'), {self.lookup_kwarg_year: str(today.year),
+ self.lookup_kwarg_month: str(today.month),
+ self.lookup_kwarg_day: str(today.day)}),
+ (_('Past 7 days'), {self.lookup_kwarg_past_7_days_gte:
one_week_ago.strftime('%Y-%m-%d'),
- '%s__lte' % self.field_path: today_str}),
- (_('This month'), {'%s__year' % self.field_path: str(today.year),
- '%s__month' % self.field_path: str(today.month)}),
- (_('This year'), {'%s__year' % self.field_path: str(today.year)})
+ self.lookup_kwarg_past_7_days_lte:
+ today_str}),
+ (_('This month'), {self.lookup_kwarg_year: str(today.year),
+ self.lookup_kwarg_month: str(today.month)}),
+ (_('This year'), {self.lookup_kwarg_year: str(today.year)})
)
- def title(self):
- return self.field.verbose_name
-
- def choices(self, cl):
+ def used_params(self):
+ return [self.lookup_kwarg_year, self.lookup_kwarg_month,
+ self.lookup_kwarg_day, self.lookup_kwarg_past_7_days_gte,
+ self.lookup_kwarg_past_7_days_lte]
+
+ def get_query_set(self, request, queryset):
+ """
+ Override the default behaviour since there can be multiple query
+ string parameters used for the same date filter (e.g. year + month).
+ """
+ query_dict = {}
+ for p in self.used_params():
+ if p in self.params:
+ query_dict[p] = self.params[p]
+ if len(query_dict):
+ return queryset.filter(**query_dict)
+
+ def _choices(self, cl):
for title, param_dict in self.links:
yield {'selected': self.date_params == param_dict,
'query_string': cl.get_query_string(
@@ -216,19 +336,19 @@ class DateFieldFilterSpec(FilterSpec):
[self.field_generic]),
'display': title}
-FilterSpec.register(lambda f: isinstance(f, models.DateField),
- DateFieldFilterSpec)
+FieldListFilter.register(lambda f: isinstance(f, models.DateField),
+ DateFieldListFilter)
# This should be registered last, because it's a last resort. For example,
-# if a field is eligible to use the BooleanFieldFilterSpec, that'd be much
-# more appropriate, and the AllValuesFilterSpec won't get used for it.
-class AllValuesFilterSpec(FilterSpec):
+# if a field is eligible to use the BooleanFieldListFilter, that'd be much
+# more appropriate, and the AllValuesFieldListFilter won't get used for it.
+class AllValuesFieldListFilter(FieldListFilter):
def __init__(self, f, request, params, model, model_admin,
- field_path=None):
- super(AllValuesFilterSpec, self).__init__(f, request, params, model,
+ field_path):
+ super(AllValuesFieldListFilter, self).__init__(f, request, params, model,
model_admin,
- field_path=field_path)
+ field_path)
self.lookup_kwarg = self.field_path
self.lookup_kwarg_isnull = '%s__isnull' % self.field_path
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
@@ -245,10 +365,10 @@ class AllValuesFilterSpec(FilterSpec):
self.lookup_choices = \
queryset.distinct().order_by(f.name).values_list(f.name, flat=True)
- def title(self):
- return self.field.verbose_name
-
- def choices(self, cl):
+ def used_params(self):
+ return [self.lookup_kwarg, self.lookup_kwarg_isnull]
+
+ def _choices(self, cl):
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
yield {'selected': self.lookup_val is None
and self.lookup_val_isnull is None,
@@ -276,4 +396,4 @@ class AllValuesFilterSpec(FilterSpec):
[self.lookup_kwarg]),
'display': EMPTY_CHANGELIST_VALUE}
-FilterSpec.register(lambda f: True, AllValuesFilterSpec)
+FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index fbda8b7..cd91273 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -1075,7 +1075,7 @@ class ModelAdmin(BaseModelAdmin):
if (actions and request.method == 'POST' and
'index' in request.POST and '_save' not in request.POST):
if selected:
- response = self.response_action(request, queryset=cl.get_query_set())
+ response = self.response_action(request, queryset=cl.get_query_set(request))
if response:
return response
else:
@@ -1091,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin):
helpers.ACTION_CHECKBOX_NAME in request.POST and
'index' not in request.POST and '_save' not in request.POST):
if selected:
- response = self.response_action(request, queryset=cl.get_query_set())
+ response = self.response_action(request, queryset=cl.get_query_set(request))
if response:
return response
else:
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index fdf082b..4919eda 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -317,7 +317,7 @@ def search_form(cl):
search_form = register.inclusion_tag('admin/search_form.html')(search_form)
def admin_list_filter(cl, spec):
- return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
+ return {'title': spec.title, 'choices' : list(spec._choices(cl))}
admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
def admin_actions(context):
diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
index 159afa4..40d14f6 100644
--- a/django/contrib/admin/validation.py
+++ b/django/contrib/admin/validation.py
@@ -3,6 +3,7 @@ from django.db import models
from django.db.models.fields import FieldDoesNotExist
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
_get_foreign_key)
+from django.contrib.admin.filterspecs import ListFilter, FieldListFilter
from django.contrib.admin.util import get_fields_from_path, NotRelationField
from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
HORIZONTAL, VERTICAL)
@@ -54,15 +55,37 @@ def validate(cls, model):
# list_filter
if hasattr(cls, 'list_filter'):
check_isseq(cls, 'list_filter', cls.list_filter)
- for idx, fpath in enumerate(cls.list_filter):
- try:
- get_fields_from_path(model, fpath)
- except (NotRelationField, FieldDoesNotExist), e:
- raise ImproperlyConfigured(
- "'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
- cls.__name__, idx, fpath
- )
- )
+ for idx, item in enumerate(cls.list_filter):
+ # There are three methods for specifying a filter:
+ # 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
+ # 2: ('field', SomeFieldListFilter) - a field-based list filter class
+ # 3: SomeListFilter - a non-field list filter class
+ if callable(item) and not isinstance(item, models.Field):
+ # If item is option 3, it should be a ListFilter.
+ if not issubclass(item, ListFilter):
+ raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
+ " which is not of type ListFilter."
+ % (cls.__name__, idx, item.__name__))
+ else:
+ try:
+ # Check for option #2 (tuple)
+ field, list_filter_class = item
+ except (TypeError, ValueError):
+ # item is option #1
+ field = item
+ else:
+ # item is option #2
+ if not issubclass(list_filter_class, FieldListFilter):
+ raise ImproperlyConfigured("'%s.list_filter[%d][1]'"
+ " is '%s' which is not of type FieldListFilter."
+ % (cls.__name__, idx, list_filter_class.__name__))
+ # Validate the field string
+ try:
+ get_fields_from_path(model, field)
+ except (NotRelationField, FieldDoesNotExist), e:
+ raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
+ " which does not refer to a Field."
+ % (cls.__name__, idx, field))
# list_per_page = 100
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 170d168..d854703 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -1,4 +1,4 @@
-from django.contrib.admin.filterspecs import FilterSpec
+from django.contrib.admin.filterspecs import ListFilter, FieldListFilter
from django.contrib.admin.options import IncorrectLookupParameters
from django.contrib.admin.util import quote, get_fields_from_path
from django.core.exceptions import SuspiciousOperation
@@ -61,20 +61,33 @@ class ChangeList(object):
self.list_editable = list_editable
self.order_field, self.order_type = self.get_ordering()
self.query = request.GET.get(SEARCH_VAR, '')
- self.query_set = self.get_query_set()
+ self.query_set = self.get_query_set(request)
self.get_results(request)
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))
- self.filter_specs, self.has_filters = self.get_filters(request)
self.pk_attname = self.lookup_opts.pk.attname
def get_filters(self, request):
filter_specs = []
if self.list_filter:
- for filter_name in self.list_filter:
- field = get_fields_from_path(self.model, filter_name)[-1]
- spec = FilterSpec.create(field, request, self.params,
- self.model, self.model_admin,
- field_path=filter_name)
+ for item in self.list_filter:
+ if callable(item):
+ # This is simply a custom ListFilter class.
+ spec = item(request, self.cleaned_params, self.model, self.model_admin)
+ else:
+ field_path = None
+ try:
+ # This is custom FieldListFilter class for a given field.
+ field, field_list_filter_class = item
+ except (TypeError, ValueError):
+ # This is simply a field name, so use the default
+ # FieldListFilter class that has been registered for
+ # the type of the given field.
+ field, field_list_filter_class = item, FieldListFilter.create
+ if not isinstance(field, models.Field):
+ field_path = field
+ field = get_fields_from_path(self.model, field_path)[-1]
+ spec = field_list_filter_class(field, request, self.cleaned_params, self.model,
+ self.model_admin, field_path=field_path)
if spec and spec.has_output():
filter_specs.append(spec)
return filter_specs, bool(filter_specs)
@@ -165,8 +178,20 @@ class ChangeList(object):
if ORDER_TYPE_VAR in params and params[ORDER_TYPE_VAR] in ('asc', 'desc'):
order_type = params[ORDER_TYPE_VAR]
return order_field, order_type
-
- def get_query_set(self):
+
+ def apply_list_filters(self, request, qs, lookup_params):
+ self.filter_specs, self.has_filters = self.get_filters(request)
+ for filter_spec in self.filter_specs:
+ if filter_spec.should_be_used():
+ qs = filter_spec.get_query_set(request, qs)
+ for param in filter_spec.used_params():
+ try:
+ del lookup_params[param]
+ except KeyError:
+ pass
+ return qs
+
+ def get_query_set(self, request):
use_distinct = False
qs = self.root_query_set
@@ -187,10 +212,11 @@ class ChangeList(object):
field_name = key.split('__', 1)[0]
try:
f = self.lookup_opts.get_field_by_name(field_name)[0]
+ if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
+ use_distinct = True
except models.FieldDoesNotExist:
- raise IncorrectLookupParameters
- if hasattr(f, 'rel') and isinstance(f.rel, models.ManyToManyRel):
- use_distinct = True
+ # It might be for a non-field custom filter specs.
+ pass
# if key ends with __in, split parameter into separate values
if key.endswith('__in'):
@@ -209,8 +235,16 @@ class ChangeList(object):
raise SuspiciousOperation(
"Filtering by %s not allowed" % key
)
-
- # Apply lookup parameters from the query string.
+
+ # Keep a copy of cleaned querystring values so it can be passed to
+ # the list filters.
+ self.cleaned_params = lookup_params.copy()
+
+ # Let every list filter modify the qs and params to its liking
+ qs = self.apply_list_filters(request, qs, lookup_params)
+
+ # Apply the remaining lookup parameters from the query string (i.e.
+ # those that haven't already been processed by the filters).
try:
qs = qs.filter(**lookup_params)
# Naked except! Because we don't have any other way of validating "params".
diff --git a/django/db/models/related.py b/django/db/models/related.py
index 7734230..90995d7 100644
--- a/django/db/models/related.py
+++ b/django/db/models/related.py
@@ -27,7 +27,7 @@ class RelatedObject(object):
as SelectField choices for this field.
Analogue of django.db.models.fields.Field.get_choices, provided
- initially for utilisation by RelatedFilterSpec.
+ initially for utilisation by RelatedFieldListFilter.
"""
first_choice = include_blank and blank_choice or []
queryset = self.model._default_manager.all()
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index 415e1fe..47b987a 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -512,7 +512,7 @@ subclass::
.. note::
``list_editable`` interacts with a couple of other options in
- particular ways; you should note the following rules:
+ particular ways; you should note the following rules::
* Any field in ``list_editable`` must also be in ``list_display``.
You can't edit a field that's not displayed!
@@ -525,13 +525,87 @@ subclass::
.. attribute:: ModelAdmin.list_filter
- Set ``list_filter`` to activate filters in the right sidebar of the change
- list page of the admin. This should be a list of field names, and each
- specified field should be either a ``BooleanField``, ``CharField``,
- ``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
+ .. versionchanged:: Development version
- This example, taken from the ``django.contrib.auth.models.User`` model,
- shows how both ``list_display`` and ``list_filter`` work::
+ Set ``list_filter`` to activate filters in the right sidebar of the change
+ list page of the admin. This should be a list of elements, where each
+ element should be of one of the following types::
+
+ * a field name, where the specified field should be either a
+ ``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
+ ``IntegerField``, ``ForeignKey`` or ``ManyToManyField``.
+
+ .. versionadded:: 1.3
+
+ Field names in ``list_filter`` can also span relations
+ using the ``__`` lookup, for example::
+
+ class UserAdminWithLookup(UserAdmin):
+ list_filter = ('groups__name')
+
+ * a class inheriting from :mod:`django.contrib.admin.ListFilter`,
+ where you need to provide a few attributes and override a few
+ methods::
+
+ from django.contrib.admin import ListFilter
+ from django.db.models import Q
+
+ class DecadeBornListFilter(ListFilter):
+ # Human-readable title which will be displayed in the
+ # right sidebar just above the filter options.
+ title = u'decade born'
+
+ # This is the code name for the filter that will be used in
+ # the url query. Providing this attribute is optional. If it is
+ # not provided then a slugified version of the title will
+ # automatically be used instead (that is, 'decade-born' in this example).
+ query_parameter_name = u'decade'
+
+ def get_choices(self, request):
+ """
+ Returns a list of tuples. The first element in each tuple
+ is the coded value for the option that will appear in the
+ url query. The second element is the human-readable name
+ for the option that will appear in the right sidebar. You
+ may specify as many choices as you like, and you may even
+ vary the list of choices depending on the HttpRequest
+ object provided as argument to this method.
+ """
+ return (
+ (u'80s', u'in the eighties'),
+ (u'other', u'other'),
+ )
+
+ def get_query_set(self, request, queryset):
+ """
+ Returns the filtered queryset based on the value provided
+ in the query string and retrievable via `get_value()`.
+ This method is only called when necessary, that is, if
+ the corresponding parameter is present in the query string.
+ The HttpRequest object is also provided for your convenience
+ in case you need to modify the list of results for each request.
+ """
+ # First, retrieve the requested value (either '80s' or 'other').
+ decade = self.get_value()
+ # Then decide how to filter the queryset based on that value.
+ if decade == u'80s':
+ return queryset.filter(birthday__year__gte=1980,
+ birthday__year__lte=1989)
+ if decade == u'other':
+ return queryset.filter(Q(year__lte=1979) |
+ Q(year__gte=1990)
+
+ class PersonAdmin(ModelAdmin):
+ list_filter = (DecadeBornListFilter,)
+
+ * a tuple, where the first element is a field name and the second
+ element is a class inheriting from
+ :mod:`django.contrib.admin.FieldListFilter`. Note that the
+ `FieldListFilter` API is currently considered internal and prone to
+ refactoring.
+
+ Finally, the following example, taken from the ``django.contrib.auth.models.User``
+ model, shows how both ``list_display`` and ``list_filter`` work::
class UserAdmin(admin.ModelAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
@@ -543,13 +617,6 @@ subclass::
(This example also has ``search_fields`` defined. See below.)
- .. versionadded:: 1.3
-
- Fields in ``list_filter`` can also span relations using the ``__`` lookup::
-
- class UserAdminWithLookup(UserAdmin):
- list_filter = ('groups__name')
-
.. attribute:: ModelAdmin.list_per_page
Set ``list_per_page`` to control how many items appear on each paginated
diff --git a/tests/regressiontests/admin_filterspecs/models.py b/tests/regressiontests/admin_filterspecs/models.py
index 5b284c7..db9912f 100644
--- a/tests/regressiontests/admin_filterspecs/models.py
+++ b/tests/regressiontests/admin_filterspecs/models.py
@@ -6,18 +6,8 @@ class Book(models.Model):
year = models.PositiveIntegerField(null=True, blank=True)
author = models.ForeignKey(User, related_name='books_authored', blank=True, null=True)
contributors = models.ManyToManyField(User, related_name='books_contributed', blank=True, null=True)
-
+ is_best_seller = models.NullBooleanField(default=0)
+ date_registered = models.DateField(null=True)
+
def __unicode__(self):
return self.title
-
-class BoolTest(models.Model):
- NO = False
- YES = True
- YES_NO_CHOICES = (
- (NO, 'no'),
- (YES, 'yes')
- )
- completed = models.BooleanField(
- default=NO,
- choices=YES_NO_CHOICES
- )
diff --git a/tests/regressiontests/admin_filterspecs/tests.py b/tests/regressiontests/admin_filterspecs/tests.py
index 8b9e734..e9c68dd 100644
--- a/tests/regressiontests/admin_filterspecs/tests.py
+++ b/tests/regressiontests/admin_filterspecs/tests.py
@@ -1,3 +1,6 @@
+import datetime
+import calendar
+
from django.contrib.auth.admin import UserAdmin
from django.test import TestCase
from django.test.client import RequestFactory
@@ -5,52 +8,161 @@ from django.contrib.auth.models import User
from django.contrib import admin
from django.contrib.admin.views.main import ChangeList
from django.utils.encoding import force_unicode
+from django.contrib.admin.filterspecs import (ListFilter,
+ BooleanFieldListFilter, FieldListFilter)
-from models import Book, BoolTest
+from models import Book
def select_by(dictlist, key, value):
return [x for x in dictlist if x[key] == value][0]
-class FilterSpecsTests(TestCase):
+
+
+class DecadeListFilterBase(ListFilter):
+
+ def get_choices(self, request):
+ return (
+ (u'the 90s', u'the 1990\'s'),
+ (u'the 00s', u'the 2000\'s'),
+ (u'other', u'other decades'),
+ )
+
+ def get_query_set(self, request, queryset):
+ decade = self.get_value()
+ if decade == u'the 90s':
+ return queryset.filter(year__gte=1990, year__lte=1999)
+ if decade == u'the 00s':
+ return queryset.filter(year__gte=2000, year__lte=2009)
+
+class DecadeListFilterWithTitle(DecadeListFilterBase):
+ title = u'publication decade'
+
+class DecadeListFilterWithParamName(DecadeListFilterBase):
+ title = u'another publication decade'
+ query_parameter_name = u'blah'
+
+class ListFiltersTests(TestCase):
def setUp(self):
+ self.today = datetime.date.today()
+ self.one_week_ago = self.today - datetime.timedelta(days=7)
+
+ self.request_factory = RequestFactory()
+
# Users
self.alfred = User.objects.create_user('alfred', 'alfred@example.com')
self.bob = User.objects.create_user('bob', 'bob@example.com')
- lisa = User.objects.create_user('lisa', 'lisa@example.com')
-
- #Books
- self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred)
- self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob)
- gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002)
- gipsy_book.contributors = [self.bob, lisa]
- gipsy_book.save()
-
- # BoolTests
- self.trueTest = BoolTest.objects.create(completed=True)
- self.falseTest = BoolTest.objects.create(completed=False)
-
- self.request_factory = RequestFactory()
+ self.lisa = User.objects.create_user('lisa', 'lisa@example.com')
+ # Books
+ 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)
+ self.bio_book = Book.objects.create(title='Django: a biography', year=1999, author=self.alfred, is_best_seller=False)
+ self.django_book = Book.objects.create(title='The Django Book', year=None, author=self.bob, is_best_seller=None, date_registered=self.today)
+ self.gipsy_book = Book.objects.create(title='Gipsy guitar for dummies', year=2002, is_best_seller=True, date_registered=self.one_week_ago)
+ self.gipsy_book.contributors = [self.bob, self.lisa]
+ self.gipsy_book.save()
def get_changelist(self, request, model, modeladmin):
return ChangeList(request, model, modeladmin.list_display, modeladmin.list_display_links,
modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
- def test_AllValuesFilterSpec(self):
+ def test_DateFieldListFilter(self):
+ modeladmin = BookAdmin(Book, admin.site)
+
+ request = self.request_factory.get('/')
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ request = self.request_factory.get('/', {'date_registered__year': self.today.year,
+ 'date_registered__month': self.today.month,
+ 'date_registered__day': self.today.day})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][4]
+ self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+ choice = select_by(filterspec._choices(changelist), "display", "Today")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?date_registered__day=%s'
+ '&date_registered__month=%s'
+ '&date_registered__year=%s'
+ % (self.today.day, self.today.month, self.today.year))
+
+ request = self.request_factory.get('/', {'date_registered__year': self.today.year,
+ 'date_registered__month': self.today.month})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ if (self.today.year, self.today.month) == (self.one_week_ago.year, self.one_week_ago.month):
+ # In case one week ago is in the same month.
+ self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+ else:
+ self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][4]
+ self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+ choice = select_by(filterspec._choices(changelist), "display", "This month")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?date_registered__month=%s'
+ '&date_registered__year=%s'
+ % (self.today.month, self.today.year))
+
+ request = self.request_factory.get('/', {'date_registered__year': self.today.year})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ if self.today.year == self.one_week_ago.year:
+ # In case one week ago is in the same year.
+ self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+ else:
+ self.assertEqual(list(queryset), [self.django_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][4]
+ self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+ choice = select_by(filterspec._choices(changelist), "display", "This year")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?date_registered__year=%s'
+ % (self.today.year))
+
+ request = self.request_factory.get('/', {'date_registered__gte': self.one_week_ago.strftime('%Y-%m-%d'),
+ 'date_registered__lte': self.today.strftime('%Y-%m-%d')})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.gipsy_book, self.django_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][4]
+ self.assertEqual(force_unicode(filterspec.title), u'date_registered')
+ choice = select_by(filterspec._choices(changelist), "display", "Past 7 days")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?date_registered__gte=%s'
+ '&date_registered__lte=%s'
+ % (self.one_week_ago.strftime('%Y-%m-%d'), self.today.strftime('%Y-%m-%d')))
+
+ def test_AllValuesFieldListFilter(self):
modeladmin = BookAdmin(Book, admin.site)
request = self.request_factory.get('/', {'year__isnull': 'True'})
changelist = self.get_changelist(request, Book, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.django_book])
# Make sure the last choice is None and is selected
filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'year')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'year')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[-1]['selected'], True)
self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
@@ -59,26 +171,25 @@ class FilterSpecsTests(TestCase):
# Make sure the correct choice is selected
filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'year')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'year')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[2]['selected'], True)
self.assertEqual(choices[2]['query_string'], '?year=2002')
- def test_RelatedFilterSpec_ForeignKey(self):
+ def test_RelatedFieldListFilter_ForeignKey(self):
modeladmin = BookAdmin(Book, admin.site)
request = self.request_factory.get('/', {'author__isnull': 'True'})
- changelist = ChangeList(request, Book, modeladmin.list_display, modeladmin.list_display_links,
- modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
- modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
+ changelist = self.get_changelist(request, Book, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.gipsy_book])
# Make sure the last choice is None and is selected
filterspec = changelist.get_filters(request)[0][1]
- self.assertEqual(force_unicode(filterspec.title()), u'author')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'author')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[-1]['selected'], True)
self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
@@ -87,25 +198,26 @@ class FilterSpecsTests(TestCase):
# Make sure the correct choice is selected
filterspec = changelist.get_filters(request)[0][1]
- self.assertEqual(force_unicode(filterspec.title()), u'author')
+ self.assertEqual(force_unicode(filterspec.title), u'author')
# order of choices depends on User model, which has no order
- choice = select_by(filterspec.choices(changelist), "display", "alfred")
+ choice = select_by(filterspec._choices(changelist), "display", "alfred")
self.assertEqual(choice['selected'], True)
self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
- def test_RelatedFilterSpec_ManyToMany(self):
+ def test_RelatedFieldListFilter_ManyToMany(self):
modeladmin = BookAdmin(Book, admin.site)
request = self.request_factory.get('/', {'contributors__isnull': 'True'})
changelist = self.get_changelist(request, Book, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.django_book, self.bio_book, self.djangonaut_book])
# Make sure the last choice is None and is selected
filterspec = changelist.get_filters(request)[0][2]
- self.assertEqual(force_unicode(filterspec.title()), u'user')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'user')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[-1]['selected'], True)
self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
@@ -114,26 +226,26 @@ class FilterSpecsTests(TestCase):
# Make sure the correct choice is selected
filterspec = changelist.get_filters(request)[0][2]
- self.assertEqual(force_unicode(filterspec.title()), u'user')
- choice = select_by(filterspec.choices(changelist), "display", "bob")
+ self.assertEqual(force_unicode(filterspec.title), u'user')
+ choice = select_by(filterspec._choices(changelist), "display", "bob")
self.assertEqual(choice['selected'], True)
self.assertEqual(choice['query_string'], '?contributors__id__exact=%d' % self.bob.pk)
-
- def test_RelatedFilterSpec_reverse_relationships(self):
+ def test_RelatedFieldListFilter_reverse_relationships(self):
modeladmin = CustomUserAdmin(User, admin.site)
# FK relationship -----
request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
changelist = self.get_changelist(request, User, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.lisa])
# Make sure the last choice is None and is selected
filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'book')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'book')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[-1]['selected'], True)
self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
@@ -142,8 +254,8 @@ class FilterSpecsTests(TestCase):
# Make sure the correct choice is selected
filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'book')
- choice = select_by(filterspec.choices(changelist), "display", self.bio_book.title)
+ self.assertEqual(force_unicode(filterspec.title), u'book')
+ choice = select_by(filterspec._choices(changelist), "display", self.bio_book.title)
self.assertEqual(choice['selected'], True)
self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
@@ -151,13 +263,14 @@ class FilterSpecsTests(TestCase):
request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
changelist = self.get_changelist(request, User, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.alfred])
# Make sure the last choice is None and is selected
filterspec = changelist.get_filters(request)[0][1]
- self.assertEqual(force_unicode(filterspec.title()), u'book')
- choices = list(filterspec.choices(changelist))
+ self.assertEqual(force_unicode(filterspec.title), u'book')
+ choices = list(filterspec._choices(changelist))
self.assertEqual(choices[-1]['selected'], True)
self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
@@ -166,46 +279,162 @@ class FilterSpecsTests(TestCase):
# Make sure the correct choice is selected
filterspec = changelist.get_filters(request)[0][1]
- self.assertEqual(force_unicode(filterspec.title()), u'book')
- choice = select_by(filterspec.choices(changelist), "display", self.django_book.title)
+ self.assertEqual(force_unicode(filterspec.title), u'book')
+ choice = select_by(filterspec._choices(changelist), "display", self.django_book.title)
self.assertEqual(choice['selected'], True)
self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
- def test_BooleanFilterSpec(self):
- modeladmin = BoolTestAdmin(BoolTest, admin.site)
-
+ def test_BooleanFieldListFilter(self):
+ modeladmin = BookAdmin(Book, admin.site)
+ self.verify_BooleanFieldListFilter(modeladmin)
+
+ def test_BooleanFieldListFilter_Tuple(self):
+ modeladmin = BookAdmin(Book, admin.site)
+ self.verify_BooleanFieldListFilter(modeladmin)
+
+ def verify_BooleanFieldListFilter(self, modeladmin):
request = self.request_factory.get('/')
- changelist = ChangeList(request, BoolTest, modeladmin.list_display, modeladmin.list_display_links,
- modeladmin.list_filter, modeladmin.date_hierarchy, modeladmin.search_fields,
- modeladmin.list_select_related, modeladmin.list_per_page, modeladmin.list_editable, modeladmin)
+ changelist = self.get_changelist(request, Book, modeladmin)
- # Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
- queryset = changelist.get_query_set()
+ request = self.request_factory.get('/', {'is_best_seller__exact': 0})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.bio_book])
- # Make sure the last choice is None and is selected
- filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'completed')
- choices = list(filterspec.choices(changelist))
- self.assertEqual(choices[-1]['selected'], False)
- self.assertEqual(choices[-1]['query_string'], '?completed__exact=0')
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+ choice = select_by(filterspec._choices(changelist), "display", "No")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?is_best_seller__exact=0')
- request = self.request_factory.get('/', {'completed__exact': 1})
- changelist = self.get_changelist(request, BoolTest, modeladmin)
+ request = self.request_factory.get('/', {'is_best_seller__exact': 1})
+ changelist = self.get_changelist(request, Book, modeladmin)
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
+
# Make sure the correct choice is selected
- filterspec = changelist.get_filters(request)[0][0]
- self.assertEqual(force_unicode(filterspec.title()), u'completed')
- # order of choices depends on User model, which has no order
- choice = select_by(filterspec.choices(changelist), "display", "Yes")
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+ choice = select_by(filterspec._choices(changelist), "display", "Yes")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?is_best_seller__exact=1')
+
+ request = self.request_factory.get('/', {'is_best_seller__isnull': 'True'})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.django_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][3]
+ self.assertEqual(force_unicode(filterspec.title), u'is_best_seller')
+ choice = select_by(filterspec._choices(changelist), "display", "Unknown")
self.assertEqual(choice['selected'], True)
- self.assertEqual(choice['query_string'], '?completed__exact=1')
+ self.assertEqual(choice['query_string'], '?is_best_seller__isnull=True')
+
+ def test_ListFilter(self):
+ modeladmin = DecadeFilterBookAdmin(Book, admin.site)
+
+ # Make sure that the first option is 'All' ---------------------------
+
+ request = self.request_factory.get('/', {})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), list(Book.objects.all().order_by('-id')))
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][1]
+ self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+ choices = list(filterspec._choices(changelist))
+ self.assertEqual(choices[0]['display'], u'All')
+ self.assertEqual(choices[0]['selected'], True)
+ self.assertEqual(choices[0]['query_string'], '?')
+
+ # Make sure that one can override the query parameter name -----------
+
+ request = self.request_factory.get('/', {'blah': 'the 90s'})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][2]
+ self.assertEqual(force_unicode(filterspec.title), u'another publication decade')
+ choices = list(filterspec._choices(changelist))
+ self.assertEqual(choices[1]['display'], u'the 1990\'s')
+ self.assertEqual(choices[1]['selected'], True)
+ self.assertEqual(choices[1]['query_string'], '?blah=the+90s')
+
+ # Look for books in the 1990s ----------------------------------------
+
+ request = self.request_factory.get('/', {'publication-decade': 'the 90s'})
+ changelist = self.get_changelist(request, Book, modeladmin)
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.bio_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][1]
+ self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+ choices = list(filterspec._choices(changelist))
+ self.assertEqual(choices[1]['display'], u'the 1990\'s')
+ self.assertEqual(choices[1]['selected'], True)
+ self.assertEqual(choices[1]['query_string'], '?publication-decade=the+90s')
+
+ # Look for books in the 2000s ----------------------------------------
+
+ request = self.request_factory.get('/', {'publication-decade': 'the 00s'})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.gipsy_book, self.djangonaut_book])
+
+ # Make sure the correct choice is selected
+ filterspec = changelist.get_filters(request)[0][1]
+ self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+ choices = list(filterspec._choices(changelist))
+ self.assertEqual(choices[2]['display'], u'the 2000\'s')
+ self.assertEqual(choices[2]['selected'], True)
+ self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s')
+
+ # Combine multiple filters -------------------------------------------
+
+ request = self.request_factory.get('/', {'publication-decade': 'the 00s', 'author__id__exact': self.alfred.pk})
+ changelist = self.get_changelist(request, Book, modeladmin)
+
+ # Make sure the correct queryset is returned
+ queryset = changelist.get_query_set(request)
+ self.assertEqual(list(queryset), [self.djangonaut_book])
+
+ # Make sure the correct choices are selected
+ filterspec = changelist.get_filters(request)[0][1]
+ self.assertEqual(force_unicode(filterspec.title), u'publication decade')
+ choices = list(filterspec._choices(changelist))
+ self.assertEqual(choices[2]['display'], u'the 2000\'s')
+ self.assertEqual(choices[2]['selected'], True)
+ self.assertEqual(choices[2]['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
+
+ filterspec = changelist.get_filters(request)[0][0]
+ self.assertEqual(force_unicode(filterspec.title), u'author')
+ choice = select_by(filterspec._choices(changelist), "display", "alfred")
+ self.assertEqual(choice['selected'], True)
+ self.assertEqual(choice['query_string'], '?publication-decade=the+00s&author__id__exact=%s' % self.alfred.pk)
+
class CustomUserAdmin(UserAdmin):
list_filter = ('books_authored', 'books_contributed')
class BookAdmin(admin.ModelAdmin):
- list_filter = ('year', 'author', 'contributors')
+ list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
+ order_by = '-id'
+
+class DecadeFilterBookAdmin(admin.ModelAdmin):
+ list_filter = ('author', DecadeListFilterWithTitle, DecadeListFilterWithParamName)
order_by = '-id'
-
-class BoolTestAdmin(admin.ModelAdmin):
- list_filter = ('completed',)
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
index b65f8a4..cfa4123 100644
--- a/tests/regressiontests/admin_views/models.py
+++ b/tests/regressiontests/admin_views/models.py
@@ -566,7 +566,7 @@ class Gadget(models.Model):
return self.name
class CustomChangeList(ChangeList):
- def get_query_set(self):
+ def get_query_set(self, request):
return self.root_query_set.filter(pk=9999) # Does not exist
class GadgetAdmin(admin.ModelAdmin):
diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
index a20e579..c14f743 100644
--- a/tests/regressiontests/modeladmin/tests.py
+++ b/tests/regressiontests/modeladmin/tests.py
@@ -2,19 +2,21 @@ from datetime import date
from django import forms
from django.conf import settings
-from django.contrib.admin.options import ModelAdmin, TabularInline, \
- HORIZONTAL, VERTICAL
+from django.contrib.admin.options import (ModelAdmin, TabularInline,
+ HORIZONTAL, VERTICAL)
from django.contrib.admin.sites import AdminSite
from django.contrib.admin.validation import validate
from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
+from django.contrib.admin.filterspecs import (ListFilter,
+ BooleanFieldListFilter)
from django.core.exceptions import ImproperlyConfigured
from django.forms.models import BaseModelFormSet
from django.forms.widgets import Select
from django.test import TestCase
from django.utils import unittest
-from models import Band, Concert, ValidationTestModel, \
- ValidationTestInlineModel
+from models import (Band, Concert, ValidationTestModel,
+ ValidationTestInlineModel)
# None of the following tests really depend on the content of the request,
@@ -850,9 +852,55 @@ class ValidationTests(unittest.TestCase):
ValidationTestModelAdmin,
ValidationTestModel,
)
+
+ class RandomClass(object):
+ pass
+
+ class ValidationTestModelAdmin(ModelAdmin):
+ list_filter = (RandomClass,)
+
+ self.assertRaisesRegexp(
+ ImproperlyConfigured,
+ "'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not of type ListFilter.",
+ validate,
+ ValidationTestModelAdmin,
+ ValidationTestModel,
+ )
+
+ class ValidationTestModelAdmin(ModelAdmin):
+ list_filter = (('is_active', RandomClass),)
+
+ self.assertRaisesRegexp(
+ ImproperlyConfigured,
+ "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'RandomClass' which is not of type FieldListFilter.",
+ validate,
+ ValidationTestModelAdmin,
+ ValidationTestModel,
+ )
+
+ class AwesomeFilter(ListFilter):
+ def get_title(self):
+ return 'awesomeness'
+ def get_choices(self, request):
+ return (('bit', 'A bit awesome'), ('very', 'Very awesome'), )
+ def get_query_set(self, cl, qs):
+ return qs
+
+ class ValidationTestModelAdmin(ModelAdmin):
+ list_filter = (('is_active', AwesomeFilter),)
+ self.assertRaisesRegexp(
+ ImproperlyConfigured,
+ "'ValidationTestModelAdmin.list_filter\[0\]\[1\]' is 'AwesomeFilter' which is not of type FieldListFilter.",
+ validate,
+ ValidationTestModelAdmin,
+ ValidationTestModel,
+ )
+
+ # Valid declarations below -----------
+
class ValidationTestModelAdmin(ModelAdmin):
- list_filter = ('is_active',)
+ list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
validate(ValidationTestModelAdmin, ValidationTestModel)