Permalink
Browse files

Disentangled some parts of the admin ChangeList and ListFilter's inte…

…rnals. With this refactoring, the query string lookups are now processed once instead of twice and some bugs (in particular the SimpleListFilter parameter name being mistaken for a model field in some cases) are avoided.

Refs #17091.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17145 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent e71f336 commit a89b15628429da79dd47c04bd191de566ef7eee6 @jphalip jphalip committed Nov 22, 2011
@@ -13,21 +13,23 @@
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)
+ reverse_field_path, get_limit_choices_to_from_path, prepare_lookup_value)
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
+ # This dictionary will eventually contain the request's query string
+ # parameters actually used by this filter.
+ self.used_parameters = {}
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.
+ Returns True if some choices would be output for this filter.
"""
raise NotImplementedError
@@ -43,15 +45,14 @@ def queryset(self, request, queryset):
"""
raise NotImplementedError
- def used_params(self):
+ def expected_parameters(self):
"""
- Return a list of parameters to consume from the change list
- querystring.
+ Returns the list of parameter names that are expected from the
+ request's query string and that will be used by this filter.
"""
raise NotImplementedError
-
class SimpleListFilter(ListFilter):
# The parameter that should be used in the query string for that filter.
parameter_name = None
@@ -67,24 +68,28 @@ def __init__(self, request, params, model, model_admin):
if lookup_choices is None:
lookup_choices = ()
self.lookup_choices = list(lookup_choices)
+ if self.parameter_name in params:
+ value = params.pop(self.parameter_name)
+ self.used_parameters[self.parameter_name] = value
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.
+ Returns the value (in string format) provided in the request's
+ query string for this filter, if any. If the value wasn't provided then
+ returns None.
"""
- return self.params.get(self.parameter_name, None)
+ return self.used_parameters.get(self.parameter_name, None)
def lookups(self, request, model_admin):
"""
Must be overriden to return a list of tuples (value, verbose value)
"""
raise NotImplementedError
- def used_params(self):
+ def expected_parameters(self):
return [self.parameter_name]
def choices(self, cl):
@@ -111,15 +116,18 @@ def __init__(self, field, request, params, model, model_admin, field_path):
self.field = field
self.field_path = field_path
self.title = getattr(field, 'verbose_name', field_path)
- super(FieldListFilter, self).__init__(request, params, model, model_admin)
+ super(FieldListFilter, self).__init__(
+ request, params, model, model_admin)
+ for p in self.expected_parameters():
+ if p in params:
+ value = params.pop(p)
+ self.used_parameters[p] = prepare_lookup_value(p, value)
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]})
+ return queryset.filter(**self.used_parameters)
@classmethod
def register(cls, test, list_filter_class, take_priority=False):
@@ -144,20 +152,20 @@ def create(cls, field, request, params, model, model_admin, 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 hasattr(field, 'verbose_name'):
- self.lookup_title = field.verbose_name
- else:
- self.lookup_title = other_model._meta.verbose_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_kwarg = '%s__%s__exact' % (field_path, rel_name)
+ self.lookup_kwarg_isnull = '%s__isnull' % 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)
+ super(RelatedFieldListFilter, self).__init__(
+ field, request, params, model, model_admin, field_path)
+ if hasattr(field, 'verbose_name'):
+ self.lookup_title = field.verbose_name
+ else:
+ self.lookup_title = other_model._meta.verbose_name
self.title = self.lookup_title
def has_output(self):
@@ -169,7 +177,7 @@ def has_output(self):
extra = 0
return len(self.lookup_choices) + extra > 1
- def used_params(self):
+ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, cl):
@@ -206,14 +214,14 @@ def choices(self, cl):
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_kwarg = '%s__exact' % field_path
+ self.lookup_kwarg2 = '%s__isnull' % field_path
self.lookup_val = request.GET.get(self.lookup_kwarg, None)
self.lookup_val2 = request.GET.get(self.lookup_kwarg2, None)
+ super(BooleanFieldListFilter, self).__init__(field,
+ request, params, model, model_admin, field_path)
- def used_params(self):
+ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg2]
def choices(self, cl):
@@ -243,12 +251,12 @@ def choices(self, cl):
class ChoicesFieldListFilter(FieldListFilter):
def __init__(self, field, request, params, model, model_admin, field_path):
+ self.lookup_kwarg = '%s__exact' % field_path
+ self.lookup_val = request.GET.get(self.lookup_kwarg)
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):
+ def expected_parameters(self):
return [self.lookup_kwarg]
def choices(self, cl):
@@ -260,7 +268,8 @@ def choices(self, cl):
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}),
+ 'query_string': cl.get_query_string({
+ self.lookup_kwarg: lookup}),
'display': title,
}
@@ -269,25 +278,19 @@ def choices(self, cl):
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.field_generic = '%s__' % 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 = str(today)
- if isinstance(self.field, models.DateTimeField):
+ if isinstance(field, models.DateTimeField):
today_str += ' 23:59:59'
-
- 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.lookup_kwarg_year = '%s__year' % field_path
+ self.lookup_kwarg_month = '%s__month' % field_path
+ self.lookup_kwarg_day = '%s__day' % field_path
+ self.lookup_kwarg_past_7_days_gte = '%s__gte' % field_path
+ self.lookup_kwarg_past_7_days_lte = '%s__lte' % field_path
self.links = (
(_('Any date'), {}),
(_('Today'), {
@@ -307,31 +310,22 @@ def __init__(self, field, request, params, model, model_admin, field_path):
self.lookup_kwarg_year: str(today.year),
}),
)
+ super(DateFieldListFilter, self).__init__(
+ field, request, params, model, model_admin, field_path)
- def used_params(self):
+ def expected_parameters(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
+ 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]),
+ param_dict, [self.field_generic]),
'display': title,
}
@@ -344,24 +338,27 @@ def choices(self, cl):
# 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_kwarg = field_path
+ self.lookup_kwarg_isnull = '%s__isnull' % 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)
+ self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull,
+ None)
+ parent_model, reverse_path = reverse_field_path(model, 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)
+ self.lookup_choices = (queryset
+ .distinct()
+ .order_by(field.name)
+ .values_list(field.name, flat=True))
+ super(AllValuesFieldListFilter, self).__init__(
+ field, request, params, model, model_admin, field_path)
- def used_params(self):
+ def expected_parameters(self):
return [self.lookup_kwarg, self.lookup_kwarg_isnull]
def choices(self, cl):
@@ -12,6 +12,33 @@
from django.utils.translation import ungettext
from django.core.urlresolvers import reverse
+def lookup_needs_distinct(opts, lookup_path):
+ """
+ Returns True if 'distinct()' should be used to query the given lookup path.
+ """
+ field_name = lookup_path.split('__', 1)[0]
+ field = opts.get_field_by_name(field_name)[0]
+ if ((hasattr(field, 'rel') and
+ isinstance(field.rel, models.ManyToManyRel)) or
+ (isinstance(field, models.related.RelatedObject) and
+ not field.field.unique)):
+ return True
+ return False
+
+def prepare_lookup_value(key, value):
+ """
+ Returns a lookup value prepared to be used in queryset filtering.
+ """
+ # if key ends with __in, split parameter into separate values
+ if key.endswith('__in'):
+ value = value.split(',')
+ # if key ends with __isnull, special case '' and false
+ if key.endswith('__isnull'):
+ if value.lower() in ('', 'false'):
+ value = False
+ else:
+ value = True
+ return value
def quote(s):
"""
Oops, something went wrong. Retry.

0 comments on commit a89b156

Please sign in to comment.