Fixed #5833 -- Modified the admin list filters to be easier to customize. Many thanks to Honza Král, Tom X. Tobin, gerdemb, eandre, sciyoshi, bendavis78 and Julien Phalip for working on this.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@16144 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
parent
a85cd1688b
commit
18d2f4a816
|
@ -4,6 +4,9 @@ from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||||
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
|
from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
|
||||||
from django.contrib.admin.options import StackedInline, TabularInline
|
from django.contrib.admin.options import StackedInline, TabularInline
|
||||||
from django.contrib.admin.sites import AdminSite, site
|
from django.contrib.admin.sites import AdminSite, site
|
||||||
|
from django.contrib.admin.filters import (ListFilter, SimpleListFilter,
|
||||||
|
FieldListFilter, BooleanFieldListFilter, RelatedFieldListFilter,
|
||||||
|
ChoicesFieldListFilter, DateFieldListFilter, AllValuesFieldListFilter)
|
||||||
|
|
||||||
|
|
||||||
def autodiscover():
|
def autodiscover():
|
||||||
|
|
|
@ -0,0 +1,398 @@
|
||||||
|
"""
|
||||||
|
This encapsulates the logic for displaying filters in the Django admin.
|
||||||
|
Filters are specified in models with the "list_filter" option.
|
||||||
|
|
||||||
|
Each filter subclass knows how to display a filter for a field that passes a
|
||||||
|
certain test -- e.g. being a DateField or ForeignKey.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.utils.encoding import smart_unicode
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib.admin.util import (get_model_from_relation,
|
||||||
|
reverse_field_path, get_limit_choices_to_from_path)
|
||||||
|
|
||||||
|
class ListFilter(object):
|
||||||
|
title = None # Human-readable title to appear in the right sidebar.
|
||||||
|
|
||||||
|
def __init__(self, request, params, model, model_admin):
|
||||||
|
self.params = params
|
||||||
|
if self.title is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The list filter '%s' does not specify "
|
||||||
|
"a 'title'." % self.__class__.__name__)
|
||||||
|
|
||||||
|
def has_output(self):
|
||||||
|
"""
|
||||||
|
Returns True if some choices would be output for the filter.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def choices(self, cl):
|
||||||
|
"""
|
||||||
|
Returns choices ready to be output in the template.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Returns the filtered queryset.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def used_params(self):
|
||||||
|
"""
|
||||||
|
Return a list of parameters to consume from the change list
|
||||||
|
querystring.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleListFilter(ListFilter):
|
||||||
|
# The parameter that should be used in the query string for that filter.
|
||||||
|
parameter_name = None
|
||||||
|
|
||||||
|
def __init__(self, request, params, model, model_admin):
|
||||||
|
super(SimpleListFilter, self).__init__(
|
||||||
|
request, params, model, model_admin)
|
||||||
|
if self.parameter_name is None:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"The list filter '%s' does not specify "
|
||||||
|
"a 'parameter_name'." % self.__class__.__name__)
|
||||||
|
self.lookup_choices = self.lookups(request)
|
||||||
|
|
||||||
|
def has_output(self):
|
||||||
|
return len(self.lookup_choices) > 0
|
||||||
|
|
||||||
|
def value(self):
|
||||||
|
"""
|
||||||
|
Returns the value given in the query string for this filter,
|
||||||
|
if any. Returns None otherwise.
|
||||||
|
"""
|
||||||
|
return self.params.get(self.parameter_name, None)
|
||||||
|
|
||||||
|
def lookups(self, request):
|
||||||
|
"""
|
||||||
|
Must be overriden to return a list of tuples (value, verbose value)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def used_params(self):
|
||||||
|
return [self.parameter_name]
|
||||||
|
|
||||||
|
def choices(self, cl):
|
||||||
|
yield {
|
||||||
|
'selected': self.value() is None,
|
||||||
|
'query_string': cl.get_query_string({}, [self.parameter_name]),
|
||||||
|
'display': _('All'),
|
||||||
|
}
|
||||||
|
for lookup, title in self.lookup_choices:
|
||||||
|
yield {
|
||||||
|
'selected': self.value() == lookup,
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.parameter_name: lookup,
|
||||||
|
}, []),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FieldListFilter(ListFilter):
|
||||||
|
_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)
|
||||||
|
|
||||||
|
def has_output(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def queryset(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))
|
||||||
|
cls._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 not test(field):
|
||||||
|
continue
|
||||||
|
return list_filter_class(field, request, params,
|
||||||
|
model, model_admin, field_path=field_path)
|
||||||
|
|
||||||
|
|
||||||
|
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 = 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 = field.get_choices(include_blank=False)
|
||||||
|
self.title = self.lookup_title
|
||||||
|
|
||||||
|
def has_output(self):
|
||||||
|
if (isinstance(self.field, models.related.RelatedObject)
|
||||||
|
and self.field.field.null or hasattr(self.field, 'rel')
|
||||||
|
and self.field.null):
|
||||||
|
extra = 1
|
||||||
|
else:
|
||||||
|
extra = 0
|
||||||
|
return len(self.lookup_choices) + extra > 1
|
||||||
|
|
||||||
|
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,
|
||||||
|
'query_string': cl.get_query_string({},
|
||||||
|
[self.lookup_kwarg, self.lookup_kwarg_isnull]),
|
||||||
|
'display': _('All'),
|
||||||
|
}
|
||||||
|
for pk_val, val in self.lookup_choices:
|
||||||
|
yield {
|
||||||
|
'selected': self.lookup_val == smart_unicode(pk_val),
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg: pk_val,
|
||||||
|
}, [self.lookup_kwarg_isnull]),
|
||||||
|
'display': val,
|
||||||
|
}
|
||||||
|
if (isinstance(self.field, models.related.RelatedObject)
|
||||||
|
and self.field.field.null or hasattr(self.field, 'rel')
|
||||||
|
and self.field.null):
|
||||||
|
yield {
|
||||||
|
'selected': bool(self.lookup_val_isnull),
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg_isnull: 'True',
|
||||||
|
}, [self.lookup_kwarg]),
|
||||||
|
'display': EMPTY_CHANGELIST_VALUE,
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldListFilter.register(lambda f: (
|
||||||
|
hasattr(f, 'rel') and bool(f.rel) or
|
||||||
|
isinstance(f, models.related.RelatedObject)), RelatedFieldListFilter)
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanFieldListFilter(FieldListFilter):
|
||||||
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
|
super(BooleanFieldListFilter, self).__init__(field,
|
||||||
|
request, params, model, model_admin, 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 used_params(self):
|
||||||
|
return [self.lookup_kwarg, self.lookup_kwarg2]
|
||||||
|
|
||||||
|
def choices(self, cl):
|
||||||
|
for lookup, title in (
|
||||||
|
(None, _('All')),
|
||||||
|
('1', _('Yes')),
|
||||||
|
('0', _('No'))):
|
||||||
|
yield {
|
||||||
|
'selected': self.lookup_val == lookup and not self.lookup_val2,
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg: lookup,
|
||||||
|
}, [self.lookup_kwarg2]),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
if isinstance(self.field, models.NullBooleanField):
|
||||||
|
yield {
|
||||||
|
'selected': self.lookup_val2 == 'True',
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg2: 'True',
|
||||||
|
}, [self.lookup_kwarg]),
|
||||||
|
'display': _('Unknown'),
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldListFilter.register(lambda f: isinstance(f,
|
||||||
|
(models.BooleanField, models.NullBooleanField)), BooleanFieldListFilter)
|
||||||
|
|
||||||
|
|
||||||
|
class ChoicesFieldListFilter(FieldListFilter):
|
||||||
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
|
super(ChoicesFieldListFilter, self).__init__(
|
||||||
|
field, request, params, model, model_admin, field_path)
|
||||||
|
self.lookup_kwarg = '%s__exact' % self.field_path
|
||||||
|
self.lookup_val = request.GET.get(self.lookup_kwarg)
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
for lookup, title in self.field.flatchoices:
|
||||||
|
yield {
|
||||||
|
'selected': smart_unicode(lookup) == self.lookup_val,
|
||||||
|
'query_string': cl.get_query_string({self.lookup_kwarg: lookup}),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
|
||||||
|
|
||||||
|
|
||||||
|
class DateFieldListFilter(FieldListFilter):
|
||||||
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
|
super(DateFieldListFilter, self).__init__(
|
||||||
|
field, request, params, model, model_admin, field_path)
|
||||||
|
|
||||||
|
self.field_generic = '%s__' % self.field_path
|
||||||
|
self.date_params = dict([(k, v) for k, v in params.items()
|
||||||
|
if k.startswith(self.field_generic)])
|
||||||
|
|
||||||
|
today = datetime.date.today()
|
||||||
|
one_week_ago = today - datetime.timedelta(days=7)
|
||||||
|
today_str = (isinstance(self.field, models.DateTimeField)
|
||||||
|
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'), {
|
||||||
|
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'),
|
||||||
|
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 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 queryset(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(
|
||||||
|
param_dict, [self.field_generic]),
|
||||||
|
'display': title,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 BooleanFieldListFilter, that'd be much
|
||||||
|
# more appropriate, and the AllValuesFieldListFilter won't get used for it.
|
||||||
|
class AllValuesFieldListFilter(FieldListFilter):
|
||||||
|
def __init__(self, field, request, params, model, model_admin, field_path):
|
||||||
|
super(AllValuesFieldListFilter, self).__init__(
|
||||||
|
field, request, params, model, model_admin, 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)
|
||||||
|
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull, None)
|
||||||
|
parent_model, reverse_path = reverse_field_path(model, self.field_path)
|
||||||
|
queryset = parent_model._default_manager.all()
|
||||||
|
# optional feature: limit choices base on existing relationships
|
||||||
|
# queryset = queryset.complex_filter(
|
||||||
|
# {'%s__isnull' % reverse_path: False})
|
||||||
|
limit_choices_to = get_limit_choices_to_from_path(model, field_path)
|
||||||
|
queryset = queryset.filter(limit_choices_to)
|
||||||
|
|
||||||
|
self.lookup_choices = queryset.distinct(
|
||||||
|
).order_by(field.name).values_list(field.name, flat=True)
|
||||||
|
|
||||||
|
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),
|
||||||
|
'query_string': cl.get_query_string({},
|
||||||
|
[self.lookup_kwarg, self.lookup_kwarg_isnull]),
|
||||||
|
'display': _('All'),
|
||||||
|
}
|
||||||
|
include_none = False
|
||||||
|
for val in self.lookup_choices:
|
||||||
|
if val is None:
|
||||||
|
include_none = True
|
||||||
|
continue
|
||||||
|
val = smart_unicode(val)
|
||||||
|
yield {
|
||||||
|
'selected': self.lookup_val == val,
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg: val,
|
||||||
|
}, [self.lookup_kwarg_isnull]),
|
||||||
|
'display': val,
|
||||||
|
}
|
||||||
|
if include_none:
|
||||||
|
yield {
|
||||||
|
'selected': bool(self.lookup_val_isnull),
|
||||||
|
'query_string': cl.get_query_string({
|
||||||
|
self.lookup_kwarg_isnull: 'True',
|
||||||
|
}, [self.lookup_kwarg]),
|
||||||
|
'display': EMPTY_CHANGELIST_VALUE,
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
|
|
@ -1,279 +0,0 @@
|
||||||
"""
|
|
||||||
FilterSpec encapsulates the logic for displaying filters in the Django admin.
|
|
||||||
Filters are specified in models with the "list_filter" option.
|
|
||||||
|
|
||||||
Each filter subclass knows how to display a filter for a field that passes a
|
|
||||||
certain test -- e.g. being a DateField or ForeignKey.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
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
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
class FilterSpec(object):
|
|
||||||
filter_specs = []
|
|
||||||
def __init__(self, f, request, params, model, model_admin,
|
|
||||||
field_path=None):
|
|
||||||
self.field = f
|
|
||||||
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 has_output(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def choices(self, cl):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def title(self):
|
|
||||||
return self.field.verbose_name
|
|
||||||
|
|
||||||
def output(self, cl):
|
|
||||||
t = []
|
|
||||||
if self.has_output():
|
|
||||||
t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
|
|
||||||
|
|
||||||
for choice in self.choices(cl):
|
|
||||||
t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
|
|
||||||
((choice['selected'] and ' class="selected"' or ''),
|
|
||||||
iri_to_uri(choice['query_string']),
|
|
||||||
choice['display']))
|
|
||||||
t.append('</ul>\n\n')
|
|
||||||
return mark_safe("".join(t))
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
other_model = get_model_from_relation(f)
|
|
||||||
if isinstance(f, (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
|
|
||||||
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)
|
|
||||||
|
|
||||||
def has_output(self):
|
|
||||||
if isinstance(self.field, models.related.RelatedObject) \
|
|
||||||
and self.field.field.null or hasattr(self.field, 'rel') \
|
|
||||||
and self.field.null:
|
|
||||||
extra = 1
|
|
||||||
else:
|
|
||||||
extra = 0
|
|
||||||
return len(self.lookup_choices) + extra > 1
|
|
||||||
|
|
||||||
def title(self):
|
|
||||||
return self.lookup_title
|
|
||||||
|
|
||||||
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,
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{},
|
|
||||||
[self.lookup_kwarg, self.lookup_kwarg_isnull]),
|
|
||||||
'display': _('All')}
|
|
||||||
for pk_val, val in self.lookup_choices:
|
|
||||||
yield {'selected': self.lookup_val == smart_unicode(pk_val),
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg: pk_val},
|
|
||||||
[self.lookup_kwarg_isnull]),
|
|
||||||
'display': val}
|
|
||||||
if isinstance(self.field, models.related.RelatedObject) \
|
|
||||||
and self.field.field.null or hasattr(self.field, 'rel') \
|
|
||||||
and self.field.null:
|
|
||||||
yield {'selected': bool(self.lookup_val_isnull),
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg_isnull: 'True'},
|
|
||||||
[self.lookup_kwarg]),
|
|
||||||
'display': EMPTY_CHANGELIST_VALUE}
|
|
||||||
|
|
||||||
FilterSpec.register(lambda f: (
|
|
||||||
hasattr(f, 'rel') and bool(f.rel) or
|
|
||||||
isinstance(f, models.related.RelatedObject)), RelatedFilterSpec)
|
|
||||||
|
|
||||||
class BooleanFieldFilterSpec(FilterSpec):
|
|
||||||
def __init__(self, f, request, params, model, model_admin,
|
|
||||||
field_path=None):
|
|
||||||
super(BooleanFieldFilterSpec, self).__init__(f, request, params, model,
|
|
||||||
model_admin,
|
|
||||||
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):
|
|
||||||
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(
|
|
||||||
{self.lookup_kwarg: v},
|
|
||||||
[self.lookup_kwarg2]),
|
|
||||||
'display': k}
|
|
||||||
if isinstance(self.field, models.NullBooleanField):
|
|
||||||
yield {'selected': self.lookup_val2 == 'True',
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg2: 'True'},
|
|
||||||
[self.lookup_kwarg]),
|
|
||||||
'display': _('Unknown')}
|
|
||||||
|
|
||||||
FilterSpec.register(lambda f: isinstance(f, models.BooleanField)
|
|
||||||
or isinstance(f, models.NullBooleanField),
|
|
||||||
BooleanFieldFilterSpec)
|
|
||||||
|
|
||||||
class ChoicesFilterSpec(FilterSpec):
|
|
||||||
def __init__(self, f, request, params, model, model_admin,
|
|
||||||
field_path=None):
|
|
||||||
super(ChoicesFilterSpec, self).__init__(f, request, params, model,
|
|
||||||
model_admin,
|
|
||||||
field_path=field_path)
|
|
||||||
self.lookup_kwarg = '%s__exact' % self.field_path
|
|
||||||
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
|
|
||||||
|
|
||||||
def choices(self, cl):
|
|
||||||
yield {'selected': self.lookup_val is None,
|
|
||||||
'query_string': cl.get_query_string({}, [self.lookup_kwarg]),
|
|
||||||
'display': _('All')}
|
|
||||||
for k, v in self.field.flatchoices:
|
|
||||||
yield {'selected': smart_unicode(k) == self.lookup_val,
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg: k}),
|
|
||||||
'display': v}
|
|
||||||
|
|
||||||
FilterSpec.register(lambda f: bool(f.choices), ChoicesFilterSpec)
|
|
||||||
|
|
||||||
class DateFieldFilterSpec(FilterSpec):
|
|
||||||
def __init__(self, f, request, params, model, model_admin,
|
|
||||||
field_path=None):
|
|
||||||
super(DateFieldFilterSpec, self).__init__(f, request, params, model,
|
|
||||||
model_admin,
|
|
||||||
field_path=field_path)
|
|
||||||
|
|
||||||
self.field_generic = '%s__' % self.field_path
|
|
||||||
|
|
||||||
self.date_params = dict([(k, v) for k, v in params.items()
|
|
||||||
if k.startswith(self.field_generic)])
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
|
||||||
one_week_ago = today - datetime.timedelta(days=7)
|
|
||||||
today_str = isinstance(self.field, models.DateTimeField) \
|
|
||||||
and today.strftime('%Y-%m-%d 23:59:59') \
|
|
||||||
or today.strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
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:
|
|
||||||
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)})
|
|
||||||
)
|
|
||||||
|
|
||||||
def title(self):
|
|
||||||
return self.field.verbose_name
|
|
||||||
|
|
||||||
def choices(self, cl):
|
|
||||||
for title, param_dict in self.links:
|
|
||||||
yield {'selected': self.date_params == param_dict,
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
param_dict,
|
|
||||||
[self.field_generic]),
|
|
||||||
'display': title}
|
|
||||||
|
|
||||||
FilterSpec.register(lambda f: isinstance(f, models.DateField),
|
|
||||||
DateFieldFilterSpec)
|
|
||||||
|
|
||||||
|
|
||||||
# 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):
|
|
||||||
def __init__(self, f, request, params, model, model_admin,
|
|
||||||
field_path=None):
|
|
||||||
super(AllValuesFilterSpec, self).__init__(f, request, params, model,
|
|
||||||
model_admin,
|
|
||||||
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)
|
|
||||||
self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
|
|
||||||
None)
|
|
||||||
parent_model, reverse_path = reverse_field_path(model, self.field_path)
|
|
||||||
queryset = parent_model._default_manager.all()
|
|
||||||
# optional feature: limit choices base on existing relationships
|
|
||||||
# queryset = queryset.complex_filter(
|
|
||||||
# {'%s__isnull' % reverse_path: False})
|
|
||||||
limit_choices_to = get_limit_choices_to_from_path(model, field_path)
|
|
||||||
queryset = queryset.filter(limit_choices_to)
|
|
||||||
|
|
||||||
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):
|
|
||||||
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
|
||||||
yield {'selected': self.lookup_val is None
|
|
||||||
and self.lookup_val_isnull is None,
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{},
|
|
||||||
[self.lookup_kwarg, self.lookup_kwarg_isnull]),
|
|
||||||
'display': _('All')}
|
|
||||||
include_none = False
|
|
||||||
|
|
||||||
for val in self.lookup_choices:
|
|
||||||
if val is None:
|
|
||||||
include_none = True
|
|
||||||
continue
|
|
||||||
val = smart_unicode(val)
|
|
||||||
|
|
||||||
yield {'selected': self.lookup_val == val,
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg: val},
|
|
||||||
[self.lookup_kwarg_isnull]),
|
|
||||||
'display': val}
|
|
||||||
if include_none:
|
|
||||||
yield {'selected': bool(self.lookup_val_isnull),
|
|
||||||
'query_string': cl.get_query_string(
|
|
||||||
{self.lookup_kwarg_isnull: 'True'},
|
|
||||||
[self.lookup_kwarg]),
|
|
||||||
'display': EMPTY_CHANGELIST_VALUE}
|
|
||||||
|
|
||||||
FilterSpec.register(lambda f: True, AllValuesFilterSpec)
|
|
|
@ -1091,7 +1091,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
if (actions and request.method == 'POST' and
|
if (actions and request.method == 'POST' and
|
||||||
'index' in request.POST and '_save' not in request.POST):
|
'index' in request.POST and '_save' not in request.POST):
|
||||||
if selected:
|
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:
|
if response:
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
@ -1107,7 +1107,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
helpers.ACTION_CHECKBOX_NAME in request.POST and
|
helpers.ACTION_CHECKBOX_NAME in request.POST and
|
||||||
'index' not in request.POST and '_save' not in request.POST):
|
'index' not in request.POST and '_save' not in request.POST):
|
||||||
if selected:
|
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:
|
if response:
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -319,7 +319,7 @@ def search_form(cl):
|
||||||
|
|
||||||
@register.inclusion_tag('admin/filter.html')
|
@register.inclusion_tag('admin/filter.html')
|
||||||
def admin_list_filter(cl, spec):
|
def admin_list_filter(cl, spec):
|
||||||
return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
|
return {'title': spec.title, 'choices' : list(spec.choices(cl))}
|
||||||
|
|
||||||
@register.inclusion_tag('admin/actions.html', takes_context=True)
|
@register.inclusion_tag('admin/actions.html', takes_context=True)
|
||||||
def admin_actions(context):
|
def admin_actions(context):
|
||||||
|
|
|
@ -3,6 +3,7 @@ from django.db import models
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
from django.db.models.fields import FieldDoesNotExist
|
||||||
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
|
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
|
||||||
_get_foreign_key)
|
_get_foreign_key)
|
||||||
|
from django.contrib.admin import ListFilter, FieldListFilter
|
||||||
from django.contrib.admin.util import get_fields_from_path, NotRelationField
|
from django.contrib.admin.util import get_fields_from_path, NotRelationField
|
||||||
from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
|
from django.contrib.admin.options import (flatten_fieldsets, BaseModelAdmin,
|
||||||
HORIZONTAL, VERTICAL)
|
HORIZONTAL, VERTICAL)
|
||||||
|
@ -54,15 +55,43 @@ def validate(cls, model):
|
||||||
# list_filter
|
# list_filter
|
||||||
if hasattr(cls, 'list_filter'):
|
if hasattr(cls, 'list_filter'):
|
||||||
check_isseq(cls, 'list_filter', cls.list_filter)
|
check_isseq(cls, 'list_filter', cls.list_filter)
|
||||||
for idx, fpath in enumerate(cls.list_filter):
|
for idx, item in enumerate(cls.list_filter):
|
||||||
try:
|
# There are three options for specifying a filter:
|
||||||
get_fields_from_path(model, fpath)
|
# 1: 'field' - a basic field filter, possibly w/ relationships (eg, 'field__rel')
|
||||||
except (NotRelationField, FieldDoesNotExist), e:
|
# 2: ('field', SomeFieldListFilter) - a field-based list filter class
|
||||||
raise ImproperlyConfigured(
|
# 3: SomeListFilter - a non-field list filter class
|
||||||
"'%s.list_filter[%d]' refers to '%s' which does not refer to a Field." % (
|
if callable(item) and not isinstance(item, models.Field):
|
||||||
cls.__name__, idx, fpath
|
# 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 a descendant of ListFilter."
|
||||||
|
% (cls.__name__, idx, item.__name__))
|
||||||
|
# ... but not a FieldListFilter.
|
||||||
|
if issubclass(item, FieldListFilter):
|
||||||
|
raise ImproperlyConfigured("'%s.list_filter[%d]' is '%s'"
|
||||||
|
" which is of type FieldListFilter but is not"
|
||||||
|
" associated with a field name."
|
||||||
|
% (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):
|
||||||
|
raise ImproperlyConfigured("'%s.list_filter[%d]' refers to '%s'"
|
||||||
|
" which does not refer to a Field."
|
||||||
|
% (cls.__name__, idx, field))
|
||||||
|
|
||||||
# list_per_page = 100
|
# list_per_page = 100
|
||||||
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
|
if hasattr(cls, 'list_per_page') and not isinstance(cls.list_per_page, int):
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
from django.contrib.admin.filterspecs import FilterSpec
|
import operator
|
||||||
from django.contrib.admin.options import IncorrectLookupParameters
|
|
||||||
from django.contrib.admin.util import quote, get_fields_from_path
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.core.paginator import InvalidPage
|
from django.core.paginator import InvalidPage
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_unicode, smart_str
|
from django.utils.encoding import force_unicode, smart_str
|
||||||
from django.utils.translation import ugettext, ugettext_lazy
|
from django.utils.translation import ugettext, ugettext_lazy
|
||||||
from django.utils.http import urlencode
|
from django.utils.http import urlencode
|
||||||
import operator
|
|
||||||
|
from django.contrib.admin import FieldListFilter
|
||||||
|
from django.contrib.admin.options import IncorrectLookupParameters
|
||||||
|
from django.contrib.admin.util import quote, get_fields_from_path
|
||||||
|
|
||||||
# The system will display a "Show all" link on the change list only if the
|
# The system will display a "Show all" link on the change list only if the
|
||||||
# total result count is less than or equal to this setting.
|
# total result count is less than or equal to this setting.
|
||||||
|
@ -23,6 +25,9 @@ TO_FIELD_VAR = 't'
|
||||||
IS_POPUP_VAR = 'pop'
|
IS_POPUP_VAR = 'pop'
|
||||||
ERROR_FLAG = 'e'
|
ERROR_FLAG = 'e'
|
||||||
|
|
||||||
|
IGNORED_PARAMS = (
|
||||||
|
ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR)
|
||||||
|
|
||||||
# Text to display within change-list table cells if the value is blank.
|
# Text to display within change-list table cells if the value is blank.
|
||||||
EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
|
EMPTY_CHANGELIST_VALUE = ugettext_lazy('(None)')
|
||||||
|
|
||||||
|
@ -36,7 +41,9 @@ def field_needs_distinct(field):
|
||||||
|
|
||||||
|
|
||||||
class ChangeList(object):
|
class ChangeList(object):
|
||||||
def __init__(self, request, model, list_display, list_display_links, list_filter, date_hierarchy, search_fields, list_select_related, list_per_page, list_editable, model_admin):
|
def __init__(self, request, model, list_display, list_display_links,
|
||||||
|
list_filter, date_hierarchy, search_fields, list_select_related,
|
||||||
|
list_per_page, list_editable, model_admin):
|
||||||
self.model = model
|
self.model = model
|
||||||
self.opts = model._meta
|
self.opts = model._meta
|
||||||
self.lookup_opts = self.opts
|
self.lookup_opts = self.opts
|
||||||
|
@ -70,20 +77,39 @@ class ChangeList(object):
|
||||||
self.list_editable = list_editable
|
self.list_editable = list_editable
|
||||||
self.order_field, self.order_type = self.get_ordering()
|
self.order_field, self.order_type = self.get_ordering()
|
||||||
self.query = request.GET.get(SEARCH_VAR, '')
|
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.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))
|
if self.is_popup:
|
||||||
self.filter_specs, self.has_filters = self.get_filters(request)
|
title = ugettext('Select %s')
|
||||||
|
else:
|
||||||
|
title = ugettext('Select %s to change')
|
||||||
|
self.title = title % force_unicode(self.opts.verbose_name)
|
||||||
self.pk_attname = self.lookup_opts.pk.attname
|
self.pk_attname = self.lookup_opts.pk.attname
|
||||||
|
|
||||||
def get_filters(self, request):
|
def get_filters(self, request, use_distinct=False):
|
||||||
filter_specs = []
|
filter_specs = []
|
||||||
|
cleaned_params, use_distinct = self.get_lookup_params(use_distinct)
|
||||||
if self.list_filter:
|
if self.list_filter:
|
||||||
for filter_name in self.list_filter:
|
for list_filer in self.list_filter:
|
||||||
field = get_fields_from_path(self.model, filter_name)[-1]
|
if callable(list_filer):
|
||||||
spec = FilterSpec.create(field, request, self.params,
|
# This is simply a custom list filter class.
|
||||||
self.model, self.model_admin,
|
spec = list_filer(request, cleaned_params,
|
||||||
field_path=filter_name)
|
self.model, self.model_admin)
|
||||||
|
else:
|
||||||
|
field_path = None
|
||||||
|
try:
|
||||||
|
# This is custom FieldListFilter class for a given field.
|
||||||
|
field, field_list_filter_class = list_filer
|
||||||
|
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 = list_filer, 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, cleaned_params,
|
||||||
|
self.model, self.model_admin, field_path=field_path)
|
||||||
if spec and spec.has_output():
|
if spec and spec.has_output():
|
||||||
filter_specs.append(spec)
|
filter_specs.append(spec)
|
||||||
return filter_specs, bool(filter_specs)
|
return filter_specs, bool(filter_specs)
|
||||||
|
@ -175,14 +201,13 @@ class ChangeList(object):
|
||||||
order_type = params[ORDER_TYPE_VAR]
|
order_type = params[ORDER_TYPE_VAR]
|
||||||
return order_field, order_type
|
return order_field, order_type
|
||||||
|
|
||||||
def get_query_set(self):
|
def get_lookup_params(self, use_distinct=False):
|
||||||
use_distinct = False
|
|
||||||
|
|
||||||
qs = self.root_query_set
|
|
||||||
lookup_params = self.params.copy() # a dictionary of the query string
|
lookup_params = self.params.copy() # a dictionary of the query string
|
||||||
for i in (ALL_VAR, ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR):
|
|
||||||
if i in lookup_params:
|
for ignored in IGNORED_PARAMS:
|
||||||
del lookup_params[i]
|
if ignored in lookup_params:
|
||||||
|
del lookup_params[ignored]
|
||||||
|
|
||||||
for key, value in lookup_params.items():
|
for key, value in lookup_params.items():
|
||||||
if not isinstance(key, str):
|
if not isinstance(key, str):
|
||||||
# 'key' will be used as a keyword argument later, so Python
|
# 'key' will be used as a keyword argument later, so Python
|
||||||
|
@ -195,10 +220,11 @@ class ChangeList(object):
|
||||||
# instance
|
# instance
|
||||||
field_name = key.split('__', 1)[0]
|
field_name = key.split('__', 1)[0]
|
||||||
try:
|
try:
|
||||||
f = self.lookup_opts.get_field_by_name(field_name)[0]
|
field = self.lookup_opts.get_field_by_name(field_name)[0]
|
||||||
|
use_distinct = field_needs_distinct(field)
|
||||||
except models.FieldDoesNotExist:
|
except models.FieldDoesNotExist:
|
||||||
raise IncorrectLookupParameters
|
# It might be a custom NonFieldFilter
|
||||||
use_distinct = field_needs_distinct(f)
|
pass
|
||||||
|
|
||||||
# if key ends with __in, split parameter into separate values
|
# if key ends with __in, split parameter into separate values
|
||||||
if key.endswith('__in'):
|
if key.endswith('__in'):
|
||||||
|
@ -214,11 +240,28 @@ class ChangeList(object):
|
||||||
lookup_params[key] = value
|
lookup_params[key] = value
|
||||||
|
|
||||||
if not self.model_admin.lookup_allowed(key, value):
|
if not self.model_admin.lookup_allowed(key, value):
|
||||||
raise SuspiciousOperation(
|
raise SuspiciousOperation("Filtering by %s not allowed" % key)
|
||||||
"Filtering by %s not allowed" % key
|
|
||||||
)
|
|
||||||
|
|
||||||
# Apply lookup parameters from the query string.
|
return lookup_params, use_distinct
|
||||||
|
|
||||||
|
def get_query_set(self, request):
|
||||||
|
lookup_params, use_distinct = self.get_lookup_params(use_distinct=False)
|
||||||
|
self.filter_specs, self.has_filters = self.get_filters(request, use_distinct)
|
||||||
|
|
||||||
|
# Let every list filter modify the qs and params to its liking
|
||||||
|
qs = self.root_query_set
|
||||||
|
for filter_spec in self.filter_specs:
|
||||||
|
new_qs = filter_spec.queryset(request, qs)
|
||||||
|
if new_qs is not None:
|
||||||
|
qs = new_qs
|
||||||
|
for param in filter_spec.used_params():
|
||||||
|
try:
|
||||||
|
del lookup_params[param]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Apply the remaining lookup parameters from the query string (i.e.
|
||||||
|
# those that haven't already been processed by the filters).
|
||||||
try:
|
try:
|
||||||
qs = qs.filter(**lookup_params)
|
qs = qs.filter(**lookup_params)
|
||||||
# Naked except! Because we don't have any other way of validating "params".
|
# Naked except! Because we don't have any other way of validating "params".
|
||||||
|
@ -226,8 +269,8 @@ class ChangeList(object):
|
||||||
# values are not in the correct type, so we might get FieldError, ValueError,
|
# values are not in the correct type, so we might get FieldError, ValueError,
|
||||||
# ValicationError, or ? from a custom field that raises yet something else
|
# ValicationError, or ? from a custom field that raises yet something else
|
||||||
# when handed impossible data.
|
# when handed impossible data.
|
||||||
except:
|
except Exception, e:
|
||||||
raise IncorrectLookupParameters
|
raise IncorrectLookupParameters(e)
|
||||||
|
|
||||||
# Use select_related() if one of the list_display options is a field
|
# Use select_related() if one of the list_display options is a field
|
||||||
# with a relationship and the provided queryset doesn't already have
|
# with a relationship and the provided queryset doesn't already have
|
||||||
|
@ -238,11 +281,11 @@ class ChangeList(object):
|
||||||
else:
|
else:
|
||||||
for field_name in self.list_display:
|
for field_name in self.list_display:
|
||||||
try:
|
try:
|
||||||
f = self.lookup_opts.get_field(field_name)
|
field = self.lookup_opts.get_field(field_name)
|
||||||
except models.FieldDoesNotExist:
|
except models.FieldDoesNotExist:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if isinstance(f.rel, models.ManyToOneRel):
|
if isinstance(field.rel, models.ManyToOneRel):
|
||||||
qs = qs.select_related()
|
qs = qs.select_related()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class RelatedObject(object):
|
||||||
as SelectField choices for this field.
|
as SelectField choices for this field.
|
||||||
|
|
||||||
Analogue of django.db.models.fields.Field.get_choices, provided
|
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 []
|
first_choice = include_blank and blank_choice or []
|
||||||
queryset = self.model._default_manager.all()
|
queryset = self.model._default_manager.all()
|
||||||
|
|
|
@ -525,30 +525,113 @@ subclass::
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.list_filter
|
.. attribute:: ModelAdmin.list_filter
|
||||||
|
|
||||||
|
.. versionchanged:: 1.4
|
||||||
|
|
||||||
Set ``list_filter`` to activate filters in the right sidebar of the change
|
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
|
list page of the admin, as illustrated in the following screenshot:
|
||||||
specified field should be either a ``BooleanField``, ``CharField``,
|
|
||||||
``DateField``, ``DateTimeField``, ``IntegerField`` or ``ForeignKey``.
|
|
||||||
|
|
||||||
This 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')
|
|
||||||
list_filter = ('is_staff', 'is_superuser')
|
|
||||||
|
|
||||||
The above code results in an admin change list page that looks like this:
|
|
||||||
|
|
||||||
.. image:: _images/users_changelist.png
|
.. image:: _images/users_changelist.png
|
||||||
|
|
||||||
(This example also has ``search_fields`` defined. See below.)
|
``list_filter`` should be a list of elements, where each element should be
|
||||||
|
of one of the following types:
|
||||||
|
|
||||||
.. versionadded:: 1.3
|
* a field name, where the specified field should be either a
|
||||||
|
``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
|
||||||
|
``IntegerField``, ``ForeignKey`` or ``ManyToManyField``, for example::
|
||||||
|
|
||||||
Fields in ``list_filter`` can also span relations using the ``__`` lookup::
|
class PersonAdmin(ModelAdmin):
|
||||||
|
list_filter = ('is_staff', 'company')
|
||||||
|
|
||||||
class UserAdminWithLookup(UserAdmin):
|
.. versionadded:: 1.3
|
||||||
list_filter = ('groups__name')
|
|
||||||
|
Field names in ``list_filter`` can also span relations
|
||||||
|
using the ``__`` lookup, for example::
|
||||||
|
|
||||||
|
class PersonAdmin(UserAdmin):
|
||||||
|
list_filter = ('company__name',)
|
||||||
|
|
||||||
|
* a class inheriting from :mod:`django.contrib.admin.SimpleListFilter`,
|
||||||
|
which you need to provide the ``title`` and ``parameter_name``
|
||||||
|
attributes to and override the ``lookups`` and ``queryset`` methods,
|
||||||
|
e.g.::
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib.admin import SimpleListFilter
|
||||||
|
|
||||||
|
class DecadeBornListFilter(SimpleListFilter):
|
||||||
|
# Human-readable title which will be displayed in the
|
||||||
|
# right admin sidebar just above the filter options.
|
||||||
|
title = _('decade born')
|
||||||
|
|
||||||
|
# Parameter for the filter that will be used in the URL query.
|
||||||
|
parameter_name = 'decade'
|
||||||
|
|
||||||
|
def lookups(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.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
('80s', 'in the eighties'),
|
||||||
|
('other', 'other'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
"""
|
||||||
|
Returns the filtered queryset based on the value
|
||||||
|
provided in the query string and retrievable via
|
||||||
|
``value()``.
|
||||||
|
"""
|
||||||
|
# Compare the requested value (either '80s' or 'other')
|
||||||
|
# to decide how to filter the queryset.
|
||||||
|
if self.value() == '80s':
|
||||||
|
return queryset.filter(birthday__year__gte=1980,
|
||||||
|
birthday__year__lte=1989)
|
||||||
|
if self.value() == 'other':
|
||||||
|
return queryset.filter(Q(year__lte=1979) |
|
||||||
|
Q(year__gte=1990))
|
||||||
|
|
||||||
|
class PersonAdmin(ModelAdmin):
|
||||||
|
list_filter = (DecadeBornListFilter,)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
As a convenience, the ``HttpRequest`` object is passed to the
|
||||||
|
filter's methods, for example::
|
||||||
|
|
||||||
|
class AuthDecadeBornListFilter(DecadeBornListFilter):
|
||||||
|
|
||||||
|
def lookups(self, request):
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
return (
|
||||||
|
('80s', 'in the eighties'),
|
||||||
|
('other', 'other'),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
('90s', 'in the nineties'),
|
||||||
|
)
|
||||||
|
|
||||||
|
* a tuple, where the first element is a field name and the second
|
||||||
|
element is a class inheriting from
|
||||||
|
:mod:`django.contrib.admin.FieldListFilter`, for example::
|
||||||
|
|
||||||
|
from django.contrib.admin import BooleanFieldListFilter
|
||||||
|
|
||||||
|
class PersonAdmin(ModelAdmin):
|
||||||
|
list_filter = (
|
||||||
|
('is_staff', BooleanFieldListFilter),
|
||||||
|
)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The ``FieldListFilter`` API is currently considered internal
|
||||||
|
and prone to refactoring.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.list_per_page
|
.. attribute:: ModelAdmin.list_per_page
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,15 @@ compatibility with old browsers, this change means that you can use any HTML5
|
||||||
features you need in admin pages without having to lose HTML validity or
|
features you need in admin pages without having to lose HTML validity or
|
||||||
override the provided templates to change the doctype.
|
override the provided templates to change the doctype.
|
||||||
|
|
||||||
|
List filters in admin interface
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Prior to Django 1.4, the Django admin app allowed specifying change list
|
||||||
|
filters by specifying a field lookup (including spanning relations), and
|
||||||
|
not custom filters. This has been rectified with a simple API previously
|
||||||
|
known as "FilterSpec" which was used internally. For more details, see the
|
||||||
|
documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
|
||||||
|
|
||||||
``reverse_lazy``
|
``reverse_lazy``
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -2,22 +2,12 @@ from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
title = models.CharField(max_length=25)
|
title = models.CharField(max_length=50)
|
||||||
year = models.PositiveIntegerField(null=True, blank=True)
|
year = models.PositiveIntegerField(null=True, blank=True)
|
||||||
author = models.ForeignKey(User, related_name='books_authored', blank=True, null=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)
|
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):
|
def __unicode__(self):
|
||||||
return self.title
|
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
|
|
||||||
)
|
|
|
@ -0,0 +1,455 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
from django.utils.encoding import force_unicode
|
||||||
|
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.admin.views.main import ChangeList
|
||||||
|
from django.contrib.admin import site, ModelAdmin, SimpleListFilter
|
||||||
|
|
||||||
|
from models import Book
|
||||||
|
|
||||||
|
def select_by(dictlist, key, value):
|
||||||
|
return [x for x in dictlist if x[key] == value][0]
|
||||||
|
|
||||||
|
|
||||||
|
class DecadeListFilter(SimpleListFilter):
|
||||||
|
|
||||||
|
def lookups(self, request):
|
||||||
|
return (
|
||||||
|
('the 90s', "the 1990's"),
|
||||||
|
('the 00s', "the 2000's"),
|
||||||
|
('other', "other decades"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def queryset(self, request, queryset):
|
||||||
|
decade = self.value()
|
||||||
|
if decade == 'the 90s':
|
||||||
|
return queryset.filter(year__gte=1990, year__lte=1999)
|
||||||
|
if decade == 'the 00s':
|
||||||
|
return queryset.filter(year__gte=2000, year__lte=2009)
|
||||||
|
|
||||||
|
class DecadeListFilterWithTitleAndParameter(DecadeListFilter):
|
||||||
|
title = 'publication decade'
|
||||||
|
parameter_name = 'publication-decade'
|
||||||
|
|
||||||
|
class DecadeListFilterWithoutTitle(DecadeListFilter):
|
||||||
|
parameter_name = 'publication-decade'
|
||||||
|
|
||||||
|
class DecadeListFilterWithoutParameter(DecadeListFilter):
|
||||||
|
title = 'publication decade'
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
list_filter = ('books_authored', 'books_contributed')
|
||||||
|
|
||||||
|
class BookAdmin(ModelAdmin):
|
||||||
|
list_filter = ('year', 'author', 'contributors', 'is_best_seller', 'date_registered')
|
||||||
|
order_by = '-id'
|
||||||
|
|
||||||
|
class DecadeFilterBookAdmin(ModelAdmin):
|
||||||
|
list_filter = ('author', DecadeListFilterWithTitleAndParameter)
|
||||||
|
order_by = '-id'
|
||||||
|
|
||||||
|
class DecadeFilterBookAdminWithoutTitle(ModelAdmin):
|
||||||
|
list_filter = (DecadeListFilterWithoutTitle,)
|
||||||
|
|
||||||
|
class DecadeFilterBookAdminWithoutParameter(ModelAdmin):
|
||||||
|
list_filter = (DecadeListFilterWithoutParameter,)
|
||||||
|
|
||||||
|
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')
|
||||||
|
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_datefieldlistfilter(self):
|
||||||
|
modeladmin = BookAdmin(Book, 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, site)
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'year__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 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(choices[-1]['selected'], True)
|
||||||
|
self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'year': '2002'})
|
||||||
|
changelist = self.get_changelist(request, Book, modeladmin)
|
||||||
|
|
||||||
|
# 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(choices[2]['selected'], True)
|
||||||
|
self.assertEqual(choices[2]['query_string'], '?year=2002')
|
||||||
|
|
||||||
|
def test_relatedfieldlistfilter_foreignkey(self):
|
||||||
|
modeladmin = BookAdmin(Book, site)
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'author__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.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(choices[-1]['selected'], True)
|
||||||
|
self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'author__id__exact': self.alfred.pk})
|
||||||
|
changelist = self.get_changelist(request, Book, modeladmin)
|
||||||
|
|
||||||
|
# Make sure the correct choice is selected
|
||||||
|
filterspec = changelist.get_filters(request)[0][1]
|
||||||
|
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")
|
||||||
|
self.assertEqual(choice['selected'], True)
|
||||||
|
self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
|
||||||
|
|
||||||
|
def test_relatedfieldlistfilter_manytomany(self):
|
||||||
|
modeladmin = BookAdmin(Book, site)
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'contributors__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, 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(choices[-1]['selected'], True)
|
||||||
|
self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'contributors__id__exact': self.bob.pk})
|
||||||
|
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'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_relatedfieldlistfilter_reverse_relationships(self):
|
||||||
|
modeladmin = CustomUserAdmin(User, site)
|
||||||
|
|
||||||
|
# FK relationship -----
|
||||||
|
request = self.request_factory.get('/', {'books_authored__isnull': 'True'})
|
||||||
|
changelist = self.get_changelist(request, User, modeladmin)
|
||||||
|
|
||||||
|
# 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(choices[-1]['selected'], True)
|
||||||
|
self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'books_authored__id__exact': self.bio_book.pk})
|
||||||
|
changelist = self.get_changelist(request, User, modeladmin)
|
||||||
|
|
||||||
|
# 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(choice['selected'], True)
|
||||||
|
self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
|
||||||
|
|
||||||
|
# M2M relationship -----
|
||||||
|
request = self.request_factory.get('/', {'books_contributed__isnull': 'True'})
|
||||||
|
changelist = self.get_changelist(request, User, modeladmin)
|
||||||
|
|
||||||
|
# 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(choices[-1]['selected'], True)
|
||||||
|
self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
|
||||||
|
|
||||||
|
request = self.request_factory.get('/', {'books_contributed__id__exact': self.django_book.pk})
|
||||||
|
changelist = self.get_changelist(request, User, modeladmin)
|
||||||
|
|
||||||
|
# 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(choice['selected'], True)
|
||||||
|
self.assertEqual(choice['query_string'], '?books_contributed__id__exact=%d' % self.django_book.pk)
|
||||||
|
|
||||||
|
def test_booleanfieldlistfilter(self):
|
||||||
|
modeladmin = BookAdmin(Book, site)
|
||||||
|
self.verify_booleanfieldlistfilter(modeladmin)
|
||||||
|
|
||||||
|
def test_booleanfieldlistfilter_tuple(self):
|
||||||
|
modeladmin = BookAdmin(Book, site)
|
||||||
|
self.verify_booleanfieldlistfilter(modeladmin)
|
||||||
|
|
||||||
|
def verify_booleanfieldlistfilter(self, modeladmin):
|
||||||
|
request = self.request_factory.get('/')
|
||||||
|
changelist = self.get_changelist(request, Book, modeladmin)
|
||||||
|
|
||||||
|
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 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('/', {'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][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'], '?is_best_seller__isnull=True')
|
||||||
|
|
||||||
|
def test_simplelistfilter(self):
|
||||||
|
modeladmin = DecadeFilterBookAdmin(Book, 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'], '?')
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def test_listfilter_without_title(self):
|
||||||
|
"""
|
||||||
|
Any filter must define a title.
|
||||||
|
"""
|
||||||
|
modeladmin = DecadeFilterBookAdminWithoutTitle(Book, site)
|
||||||
|
request = self.request_factory.get('/', {})
|
||||||
|
self.assertRaisesRegexp(ImproperlyConfigured,
|
||||||
|
"The list filter 'DecadeListFilterWithoutTitle' does not specify a 'title'.",
|
||||||
|
self.get_changelist, request, Book, modeladmin)
|
||||||
|
|
||||||
|
def test_simplelistfilter_without_parameter(self):
|
||||||
|
"""
|
||||||
|
Any SimpleListFilter must define a parameter_name.
|
||||||
|
"""
|
||||||
|
modeladmin = DecadeFilterBookAdminWithoutParameter(Book, site)
|
||||||
|
request = self.request_factory.get('/', {})
|
||||||
|
self.assertRaisesRegexp(ImproperlyConfigured,
|
||||||
|
"The list filter 'DecadeListFilterWithoutParameter' does not specify a 'parameter_name'.",
|
||||||
|
self.get_changelist, request, Book, modeladmin)
|
|
@ -1,211 +0,0 @@
|
||||||
from django.contrib.auth.admin import UserAdmin
|
|
||||||
from django.test import TestCase
|
|
||||||
from django.test.client import RequestFactory
|
|
||||||
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 models import Book, BoolTest
|
|
||||||
|
|
||||||
def select_by(dictlist, key, value):
|
|
||||||
return [x for x in dictlist if x[key] == value][0]
|
|
||||||
|
|
||||||
class FilterSpecsTests(TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
|
|
||||||
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):
|
|
||||||
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 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(choices[-1]['selected'], True)
|
|
||||||
self.assertEqual(choices[-1]['query_string'], '?year__isnull=True')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'year': '2002'})
|
|
||||||
changelist = self.get_changelist(request, Book, modeladmin)
|
|
||||||
|
|
||||||
# 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(choices[2]['selected'], True)
|
|
||||||
self.assertEqual(choices[2]['query_string'], '?year=2002')
|
|
||||||
|
|
||||||
def test_RelatedFilterSpec_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)
|
|
||||||
|
|
||||||
# Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
|
|
||||||
queryset = changelist.get_query_set()
|
|
||||||
|
|
||||||
# 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(choices[-1]['selected'], True)
|
|
||||||
self.assertEqual(choices[-1]['query_string'], '?author__isnull=True')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'author__id__exact': self.alfred.pk})
|
|
||||||
changelist = self.get_changelist(request, Book, modeladmin)
|
|
||||||
|
|
||||||
# Make sure the correct choice is selected
|
|
||||||
filterspec = changelist.get_filters(request)[0][1]
|
|
||||||
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")
|
|
||||||
self.assertEqual(choice['selected'], True)
|
|
||||||
self.assertEqual(choice['query_string'], '?author__id__exact=%d' % self.alfred.pk)
|
|
||||||
|
|
||||||
def test_RelatedFilterSpec_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 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(choices[-1]['selected'], True)
|
|
||||||
self.assertEqual(choices[-1]['query_string'], '?contributors__isnull=True')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'contributors__id__exact': self.bob.pk})
|
|
||||||
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'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):
|
|
||||||
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 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(choices[-1]['selected'], True)
|
|
||||||
self.assertEqual(choices[-1]['query_string'], '?books_authored__isnull=True')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'books_authored__id__exact': self.bio_book.pk})
|
|
||||||
changelist = self.get_changelist(request, User, modeladmin)
|
|
||||||
|
|
||||||
# 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(choice['selected'], True)
|
|
||||||
self.assertEqual(choice['query_string'], '?books_authored__id__exact=%d' % self.bio_book.pk)
|
|
||||||
|
|
||||||
# M2M relationship -----
|
|
||||||
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 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(choices[-1]['selected'], True)
|
|
||||||
self.assertEqual(choices[-1]['query_string'], '?books_contributed__isnull=True')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'books_contributed__id__exact': self.django_book.pk})
|
|
||||||
changelist = self.get_changelist(request, User, modeladmin)
|
|
||||||
|
|
||||||
# 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(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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# Make sure changelist.get_query_set() does not raise IncorrectLookupParameters
|
|
||||||
queryset = changelist.get_query_set()
|
|
||||||
|
|
||||||
# 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')
|
|
||||||
|
|
||||||
request = self.request_factory.get('/', {'completed__exact': 1})
|
|
||||||
changelist = self.get_changelist(request, BoolTest, modeladmin)
|
|
||||||
|
|
||||||
# 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")
|
|
||||||
self.assertEqual(choice['selected'], True)
|
|
||||||
self.assertEqual(choice['query_string'], '?completed__exact=1')
|
|
||||||
|
|
||||||
class CustomUserAdmin(UserAdmin):
|
|
||||||
list_filter = ('books_authored', 'books_contributed')
|
|
||||||
|
|
||||||
class BookAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = ('year', 'author', 'contributors')
|
|
||||||
order_by = '-id'
|
|
||||||
|
|
||||||
class BoolTestAdmin(admin.ModelAdmin):
|
|
||||||
list_filter = ('completed',)
|
|
|
@ -611,7 +611,7 @@ class Gadget(models.Model):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
class CustomChangeList(ChangeList):
|
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
|
return self.root_query_set.filter(pk=9999) # Does not exist
|
||||||
|
|
||||||
class GadgetAdmin(admin.ModelAdmin):
|
class GadgetAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -2,19 +2,21 @@ from datetime import date
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.admin.options import ModelAdmin, TabularInline, \
|
from django.contrib.admin.options import (ModelAdmin, TabularInline,
|
||||||
HORIZONTAL, VERTICAL
|
HORIZONTAL, VERTICAL)
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.contrib.admin.validation import validate
|
from django.contrib.admin.validation import validate
|
||||||
from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
|
from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
|
||||||
|
from django.contrib.admin import (SimpleListFilter,
|
||||||
|
BooleanFieldListFilter)
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.models import BaseModelFormSet
|
from django.forms.models import BaseModelFormSet
|
||||||
from django.forms.widgets import Select
|
from django.forms.widgets import Select
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import unittest
|
from django.utils import unittest
|
||||||
|
|
||||||
from models import Band, Concert, ValidationTestModel, \
|
from models import (Band, Concert, ValidationTestModel,
|
||||||
ValidationTestInlineModel
|
ValidationTestInlineModel)
|
||||||
|
|
||||||
|
|
||||||
# None of the following tests really depend on the content of the request,
|
# None of the following tests really depend on the content of the request,
|
||||||
|
@ -851,8 +853,65 @@ class ValidationTests(unittest.TestCase):
|
||||||
ValidationTestModel,
|
ValidationTestModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class RandomClass(object):
|
||||||
|
pass
|
||||||
|
|
||||||
class ValidationTestModelAdmin(ModelAdmin):
|
class ValidationTestModelAdmin(ModelAdmin):
|
||||||
list_filter = ('is_active',)
|
list_filter = (RandomClass,)
|
||||||
|
|
||||||
|
self.assertRaisesRegexp(
|
||||||
|
ImproperlyConfigured,
|
||||||
|
"'ValidationTestModelAdmin.list_filter\[0\]' is 'RandomClass' which is not a descendant of 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(SimpleListFilter):
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ValidationTestModelAdmin(ModelAdmin):
|
||||||
|
list_filter = (BooleanFieldListFilter,)
|
||||||
|
|
||||||
|
self.assertRaisesRegexp(
|
||||||
|
ImproperlyConfigured,
|
||||||
|
"'ValidationTestModelAdmin.list_filter\[0\]' is 'BooleanFieldListFilter' which is of type FieldListFilter but is not associated with a field name.",
|
||||||
|
validate,
|
||||||
|
ValidationTestModelAdmin,
|
||||||
|
ValidationTestModel,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Valid declarations below -----------
|
||||||
|
|
||||||
|
class ValidationTestModelAdmin(ModelAdmin):
|
||||||
|
list_filter = ('is_active', AwesomeFilter, ('is_active', BooleanFieldListFilter))
|
||||||
|
|
||||||
validate(ValidationTestModelAdmin, ValidationTestModel)
|
validate(ValidationTestModelAdmin, ValidationTestModel)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue