123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- """
- 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.contrib.admin.options import IncorrectLookupParameters
- from django.contrib.admin.utils import (
- get_model_from_relation, prepare_lookup_value, reverse_field_path,
- )
- from django.core.exceptions import ImproperlyConfigured, ValidationError
- from django.db import models
- from django.utils import timezone
- from django.utils.encoding import force_text
- from django.utils.translation import ugettext_lazy as _
- class ListFilter(object):
- title = None # Human-readable title to appear in the right sidebar.
- template = 'admin/filter.html'
- def __init__(self, request, params, model, model_admin):
- # 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 this filter.
- """
- raise NotImplementedError('subclasses of ListFilter must provide a has_output() method')
- def choices(self, changelist):
- """
- Returns choices ready to be output in the template.
- `changelist` is the ChangeList to be displayed.
- """
- raise NotImplementedError('subclasses of ListFilter must provide a choices() method')
- def queryset(self, request, queryset):
- """
- Returns the filtered queryset.
- """
- raise NotImplementedError('subclasses of ListFilter must provide a queryset() method')
- def expected_parameters(self):
- """
- 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('subclasses of ListFilter must provide an expected_parameters() method')
- 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__)
- if self.parameter_name in params:
- value = params.pop(self.parameter_name)
- self.used_parameters[self.parameter_name] = value
- lookup_choices = self.lookups(request, model_admin)
- if lookup_choices is None:
- lookup_choices = ()
- self.lookup_choices = list(lookup_choices)
- def has_output(self):
- return len(self.lookup_choices) > 0
- def value(self):
- """
- 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.used_parameters.get(self.parameter_name)
- def lookups(self, request, model_admin):
- """
- Must be overridden to return a list of tuples (value, verbose value)
- """
- raise NotImplementedError(
- 'The SimpleListFilter.lookups() method must be overridden to '
- 'return a list of tuples (value, verbose value)')
- def expected_parameters(self):
- return [self.parameter_name]
- def choices(self, changelist):
- yield {
- 'selected': self.value() is None,
- 'query_string': changelist.get_query_string({}, [self.parameter_name]),
- 'display': _('All'),
- }
- for lookup, title in self.lookup_choices:
- yield {
- 'selected': self.value() == force_text(lookup),
- 'query_string': changelist.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 = getattr(field, 'verbose_name', field_path)
- 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):
- try:
- return queryset.filter(**self.used_parameters)
- except ValidationError as e:
- raise IncorrectLookupParameters(e)
- @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):
- other_model = get_model_from_relation(field)
- self.lookup_kwarg = '%s__%s__exact' % (field_path, field.target_field.name)
- self.lookup_kwarg_isnull = '%s__isnull' % field_path
- self.lookup_val = request.GET.get(self.lookup_kwarg)
- self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
- super(RelatedFieldListFilter, self).__init__(
- field, request, params, model, model_admin, field_path)
- self.lookup_choices = self.field_choices(field, request, model_admin)
- 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
- self.empty_value_display = model_admin.get_empty_value_display()
- @property
- def include_empty_choice(self):
- """
- Return True if a "(None)" choice should be included, which filters
- out everything except empty relationships.
- """
- return self.field.null or (self.field.is_relation and self.field.many_to_many)
- def has_output(self):
- if self.include_empty_choice:
- extra = 1
- else:
- extra = 0
- return len(self.lookup_choices) + extra > 1
- def expected_parameters(self):
- return [self.lookup_kwarg, self.lookup_kwarg_isnull]
- def field_choices(self, field, request, model_admin):
- return field.get_choices(include_blank=False)
- def choices(self, changelist):
- yield {
- 'selected': self.lookup_val is None and not self.lookup_val_isnull,
- 'query_string': changelist.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 == force_text(pk_val),
- 'query_string': changelist.get_query_string({
- self.lookup_kwarg: pk_val,
- }, [self.lookup_kwarg_isnull]),
- 'display': val,
- }
- if self.include_empty_choice:
- yield {
- 'selected': bool(self.lookup_val_isnull),
- 'query_string': changelist.get_query_string({
- self.lookup_kwarg_isnull: 'True',
- }, [self.lookup_kwarg]),
- 'display': self.empty_value_display,
- }
- FieldListFilter.register(lambda f: f.remote_field, RelatedFieldListFilter)
- class BooleanFieldListFilter(FieldListFilter):
- def __init__(self, field, request, params, model, model_admin, 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)
- self.lookup_val2 = request.GET.get(self.lookup_kwarg2)
- super(BooleanFieldListFilter, self).__init__(field, request, params, model, model_admin, field_path)
- if (self.used_parameters and self.lookup_kwarg in self.used_parameters and
- self.used_parameters[self.lookup_kwarg] in ('1', '0')):
- self.used_parameters[self.lookup_kwarg] = bool(int(self.used_parameters[self.lookup_kwarg]))
- def expected_parameters(self):
- return [self.lookup_kwarg, self.lookup_kwarg2]
- def choices(self, changelist):
- for lookup, title in (
- (None, _('All')),
- ('1', _('Yes')),
- ('0', _('No'))):
- yield {
- 'selected': self.lookup_val == lookup and not self.lookup_val2,
- 'query_string': changelist.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': changelist.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):
- self.lookup_kwarg = '%s__exact' % field_path
- self.lookup_kwarg_isnull = '%s__isnull' % field_path
- self.lookup_val = request.GET.get(self.lookup_kwarg)
- self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
- super(ChoicesFieldListFilter, self).__init__(
- field, request, params, model, model_admin, field_path)
- def expected_parameters(self):
- return [self.lookup_kwarg, self.lookup_kwarg_isnull]
- def choices(self, changelist):
- yield {
- 'selected': self.lookup_val is None,
- 'query_string': changelist.get_query_string(
- {}, [self.lookup_kwarg, self.lookup_kwarg_isnull]
- ),
- 'display': _('All')
- }
- none_title = ''
- for lookup, title in self.field.flatchoices:
- if lookup is None:
- none_title = title
- continue
- yield {
- 'selected': force_text(lookup) == self.lookup_val,
- 'query_string': changelist.get_query_string(
- {self.lookup_kwarg: lookup}, [self.lookup_kwarg_isnull]
- ),
- 'display': title,
- }
- if none_title:
- yield {
- 'selected': bool(self.lookup_val_isnull),
- 'query_string': changelist.get_query_string({
- self.lookup_kwarg_isnull: 'True',
- }, [self.lookup_kwarg]),
- 'display': none_title,
- }
- FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
- class DateFieldListFilter(FieldListFilter):
- def __init__(self, field, request, params, model, model_admin, field_path):
- self.field_generic = '%s__' % field_path
- self.date_params = {k: v for k, v in params.items() if k.startswith(self.field_generic)}
- now = timezone.now()
- # When time zone support is enabled, convert "now" to the user's time
- # zone so Django's definition of "Today" matches what the user expects.
- if timezone.is_aware(now):
- now = timezone.localtime(now)
- if isinstance(field, models.DateTimeField):
- today = now.replace(hour=0, minute=0, second=0, microsecond=0)
- else: # field is a models.DateField
- today = now.date()
- tomorrow = today + datetime.timedelta(days=1)
- if today.month == 12:
- next_month = today.replace(year=today.year + 1, month=1, day=1)
- else:
- next_month = today.replace(month=today.month + 1, day=1)
- next_year = today.replace(year=today.year + 1, month=1, day=1)
- self.lookup_kwarg_since = '%s__gte' % field_path
- self.lookup_kwarg_until = '%s__lt' % field_path
- self.links = (
- (_('Any date'), {}),
- (_('Today'), {
- self.lookup_kwarg_since: str(today),
- self.lookup_kwarg_until: str(tomorrow),
- }),
- (_('Past 7 days'), {
- self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)),
- self.lookup_kwarg_until: str(tomorrow),
- }),
- (_('This month'), {
- self.lookup_kwarg_since: str(today.replace(day=1)),
- self.lookup_kwarg_until: str(next_month),
- }),
- (_('This year'), {
- self.lookup_kwarg_since: str(today.replace(month=1, day=1)),
- self.lookup_kwarg_until: str(next_year),
- }),
- )
- if field.null:
- self.lookup_kwarg_isnull = '%s__isnull' % field_path
- self.links += (
- (_('No date'), {self.field_generic + 'isnull': 'True'}),
- (_('Has date'), {self.field_generic + 'isnull': 'False'}),
- )
- super(DateFieldListFilter, self).__init__(
- field, request, params, model, model_admin, field_path)
- def expected_parameters(self):
- params = [self.lookup_kwarg_since, self.lookup_kwarg_until]
- if self.field.null:
- params.append(self.lookup_kwarg_isnull)
- return params
- def choices(self, changelist):
- for title, param_dict in self.links:
- yield {
- 'selected': self.date_params == param_dict,
- 'query_string': changelist.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):
- self.lookup_kwarg = field_path
- self.lookup_kwarg_isnull = '%s__isnull' % field_path
- self.lookup_val = request.GET.get(self.lookup_kwarg)
- self.lookup_val_isnull = request.GET.get(self.lookup_kwarg_isnull)
- self.empty_value_display = model_admin.get_empty_value_display()
- parent_model, reverse_path = reverse_field_path(model, field_path)
- # Obey parent ModelAdmin queryset when deciding which options to show
- if model == parent_model:
- queryset = model_admin.get_queryset(request)
- else:
- queryset = parent_model._default_manager.all()
- 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 expected_parameters(self):
- return [self.lookup_kwarg, self.lookup_kwarg_isnull]
- def choices(self, changelist):
- yield {
- 'selected': self.lookup_val is None and self.lookup_val_isnull is None,
- 'query_string': changelist.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 = force_text(val)
- yield {
- 'selected': self.lookup_val == val,
- 'query_string': changelist.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': changelist.get_query_string({
- self.lookup_kwarg_isnull: 'True',
- }, [self.lookup_kwarg]),
- 'display': self.empty_value_display,
- }
- FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
- class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
- def field_choices(self, field, request, model_admin):
- pk_qs = model_admin.get_queryset(request).distinct().values_list('%s__pk' % self.field_path, flat=True)
- return field.get_choices(include_blank=False, limit_choices_to={'pk__in': pk_qs})
|