debian-django-filter/django_filters/filters.py

495 lines
15 KiB
Python

from __future__ import absolute_import
from __future__ import unicode_literals
import warnings
from datetime import timedelta
from django import forms
from django.db.models import Q
from django.db.models.sql.constants import QUERY_TERMS
from django.db.models.constants import LOOKUP_SEP
from django.conf import settings
from django.utils import six
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from .fields import (
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
)
__all__ = [
'AllValuesFilter',
'BooleanFilter',
'CharFilter',
'ChoiceFilter',
'DateFilter',
'DateFromToRangeFilter',
'DateRangeFilter',
'DateTimeFilter',
'DateTimeFromToRangeFilter',
'DurationFilter',
'Filter',
'IsoDateTimeFilter',
'MethodFilter',
'ModelChoiceFilter',
'ModelMultipleChoiceFilter',
'MultipleChoiceFilter',
'NumberFilter',
'NumericRangeFilter',
'RangeFilter',
'TimeFilter',
'TimeRangeFilter',
'TypedChoiceFilter',
'UUIDFilter',
]
LOOKUP_TYPES = sorted(QUERY_TERMS)
def _lookup_type_warning():
warnings.warn('lookup_type is deprecated. Use lookup_expr instead.', DeprecationWarning, stacklevel=3)
class Filter(object):
creation_counter = 0
field_class = forms.Field
def __init__(self, name=None, label=None, widget=None, action=None,
lookup_expr='exact', required=False, distinct=False, exclude=False, **kwargs):
self.name = name
self.label = label
if action:
self.filter = action
self.lookup_expr = lookup_expr
if 'lookup_type' in kwargs:
_lookup_type_warning()
self.lookup_expr = kwargs.pop('lookup_type')
self.widget = widget
self.required = required
self.extra = kwargs
self.distinct = distinct
self.exclude = exclude
self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1
def get_method(self, qs):
"""Return filter method based on whether we're excluding
or simply filtering.
"""
return qs.exclude if self.exclude else qs.filter
def lookup_type():
def fget(self):
_lookup_type_warning()
return self.lookup_expr
def fset(self, value):
_lookup_type_warning()
self.lookup_expr = value
return locals()
lookup_type = property(**lookup_type())
@property
def field(self):
if not hasattr(self, '_field'):
help_text = self.extra.pop('help_text', None)
if help_text is None:
if self.exclude and getattr(settings, "FILTERS_HELP_TEXT_EXCLUDE", True):
help_text = _('This is an exclusion filter')
elif not self.exclude and getattr(settings, "FILTERS_HELP_TEXT_FILTER", True):
help_text = _('Filter')
else:
help_text = ''
if (self.lookup_expr is None or
isinstance(self.lookup_expr, (list, tuple))):
lookup = []
for x in LOOKUP_TYPES:
if isinstance(x, (list, tuple)) and len(x) == 2:
choice = (x[0], x[1])
else:
choice = (x, x)
if self.lookup_expr is None:
lookup.append(choice)
else:
if isinstance(x, (list, tuple)) and len(x) == 2:
if x[0] in self.lookup_expr:
lookup.append(choice)
else:
if x in self.lookup_expr:
lookup.append(choice)
self._field = LookupTypeField(self.field_class(
required=self.required, widget=self.widget, **self.extra),
lookup, required=self.required, label=self.label, help_text=help_text)
else:
self._field = self.field_class(required=self.required,
label=self.label, widget=self.widget,
help_text=help_text, **self.extra)
return self._field
def filter(self, qs, value):
if isinstance(value, Lookup):
lookup = six.text_type(value.lookup_type)
value = value.value
else:
lookup = self.lookup_expr
if value in ([], (), {}, None, ''):
return qs
if self.distinct:
qs = qs.distinct()
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
return qs
class CharFilter(Filter):
field_class = forms.CharField
class BooleanFilter(Filter):
field_class = forms.NullBooleanField
class ChoiceFilter(Filter):
field_class = forms.ChoiceField
class TypedChoiceFilter(Filter):
field_class = forms.TypedChoiceField
class UUIDFilter(Filter):
field_class = forms.UUIDField
class MultipleChoiceFilter(Filter):
"""
This filter preforms OR(by default) or AND(using conjoined=True) query
on the selected options.
Advanced Use
------------
Depending on your application logic, when all or no choices are selected,
filtering may be a noop. In this case you may wish to avoid the filtering
overhead, particularly if using a `distinct` call.
Set `always_filter` to False after instantiation to enable the default
`is_noop` test.
Override `is_noop` if you require a different test for your application.
`distinct` defaults to True on this class to preserve backward compatibility.
"""
field_class = forms.MultipleChoiceField
always_filter = True
def __init__(self, *args, **kwargs):
distinct = kwargs.get('distinct', True)
kwargs['distinct'] = distinct
conjoined = kwargs.pop('conjoined', False)
self.conjoined = conjoined
super(MultipleChoiceFilter, self).__init__(*args, **kwargs)
def is_noop(self, qs, value):
"""
Return True to short-circuit unnecessary and potentially slow filtering.
"""
if self.always_filter:
return False
# A reasonable default for being a noop...
if self.required and len(value) == len(self.field.choices):
return True
return False
def filter(self, qs, value):
value = value or () # Make sure we have an iterable
if self.is_noop(qs, value):
return qs
# Even though not a noop, no point filtering if empty
if not value:
return qs
q = Q()
for v in set(value):
if self.conjoined:
qs = self.get_method(qs)(**{self.name: v})
else:
q |= Q(**{self.name: v})
if self.distinct:
return self.get_method(qs)(q).distinct()
return self.get_method(qs)(q)
class DateFilter(Filter):
field_class = forms.DateField
class DateTimeFilter(Filter):
field_class = forms.DateTimeField
class IsoDateTimeFilter(DateTimeFilter):
"""
Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
For context see:
* https://code.djangoproject.com/ticket/23448
* https://github.com/tomchristie/django-rest-framework/issues/1338
* https://github.com/alex/django-filter/pull/264
"""
field_class = IsoDateTimeField
class TimeFilter(Filter):
field_class = forms.TimeField
class DurationFilter(Filter):
field_class = forms.DurationField
class ModelChoiceFilter(Filter):
field_class = forms.ModelChoiceField
class ModelMultipleChoiceFilter(MultipleChoiceFilter):
field_class = forms.ModelMultipleChoiceField
class NumberFilter(Filter):
field_class = forms.DecimalField
class NumericRangeFilter(Filter):
field_class = RangeField
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__%s' % (self.name, self.lookup_expr)
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__startswith' % self.name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__endswith' % self.name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
class RangeFilter(Filter):
field_class = RangeField
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__range' % self.name
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__gte' % self.name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__lte' % self.name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
def _truncate(dt):
return dt.date()
class DateRangeFilter(ChoiceFilter):
options = {
'': (_('Any date'), lambda qs, name: qs),
1: (_('Today'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: now().day
})),
2: (_('Past 7 days'), lambda qs, name: qs.filter(**{
'%s__gte' % name: _truncate(now() - timedelta(days=7)),
'%s__lt' % name: _truncate(now() + timedelta(days=1)),
})),
3: (_('This month'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month
})),
4: (_('This year'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
})),
5: (_('Yesterday'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: (now() - timedelta(days=1)).day,
})),
}
def __init__(self, *args, **kwargs):
kwargs['choices'] = [
(key, value[0]) for key, value in six.iteritems(self.options)]
super(DateRangeFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
try:
value = int(value)
except (ValueError, TypeError):
value = ''
assert value in self.options
qs = self.options[value][1](qs, self.name)
if self.distinct:
qs = qs.distinct()
return qs
class DateFromToRangeFilter(RangeFilter):
field_class = DateRangeField
class DateTimeFromToRangeFilter(RangeFilter):
field_class = DateTimeRangeField
class TimeRangeFilter(RangeFilter):
field_class = TimeRangeField
class AllValuesFilter(ChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.name).values_list(self.name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesFilter, self).field
class AllValuesMultipleFilter(MultipleChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.name).values_list(self.name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesMultipleFilter, self).field
class BaseCSVFilter(Filter):
"""
Base class for CSV type filters, such as IN and RANGE.
"""
base_field_class = BaseCSVField
def __init__(self, *args, **kwargs):
super(BaseCSVFilter, self).__init__(*args, **kwargs)
class ConcreteCSVField(self.base_field_class, self.field_class):
pass
ConcreteCSVField.__name__ = self._field_class_name(
self.field_class, self.lookup_expr
)
self.field_class = ConcreteCSVField
@classmethod
def _field_class_name(cls, field_class, lookup_expr):
"""
Generate a suitable class name for the concrete field class. This is not
completely reliable, as not all field class names are of the format
<Type>Field.
ex::
BaseCSVFilter._field_class_name(DateTimeField, 'year__in')
returns 'DateTimeYearInField'
"""
# DateTimeField => DateTime
type_name = field_class.__name__
if type_name.endswith('Field'):
type_name = type_name[:-5]
# year__in => YearIn
parts = lookup_expr.split(LOOKUP_SEP)
expression_name = ''.join(p.capitalize() for p in parts)
# DateTimeYearInField
return str('%s%sField' % (type_name, expression_name))
class BaseInFilter(BaseCSVFilter):
def __init__(self, *args, **kwargs):
kwargs.setdefault('lookup_expr', 'in')
super(BaseInFilter, self).__init__(*args, **kwargs)
class BaseRangeFilter(BaseCSVFilter):
base_field_class = BaseRangeField
def __init__(self, *args, **kwargs):
kwargs.setdefault('lookup_expr', 'range')
super(BaseRangeFilter, self).__init__(*args, **kwargs)
class MethodFilter(Filter):
"""
This filter will allow you to run a method that exists on the filterset class
"""
def __init__(self, *args, **kwargs):
# Get the action out of the kwargs
action = kwargs.get('action', None)
# If the action is a string store the action and set the action to our own filter method
# so it can be backwards compatible and work as expected, the parent will still treat it as
# a filter that has an action
self.parent_action = ''
text_types = (str, six.text_type)
if type(action) in text_types:
self.parent_action = str(action)
kwargs.update({
'action': self.filter
})
# Call the parent
super(MethodFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
"""
This filter method will act as a proxy for the actual method we want to
call.
It will try to find the method on the parent filterset,
if not it attempts to search for the method `field_{{attribute_name}}`.
Otherwise it defaults to just returning the queryset.
"""
parent = getattr(self, 'parent', None)
parent_filter_method = getattr(parent, self.parent_action, None)
if not parent_filter_method:
func_str = 'filter_{0}'.format(self.name)
parent_filter_method = getattr(parent, func_str, None)
if parent_filter_method is not None:
return parent_filter_method(qs, value)
return qs