# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
from functools import partial
import six
from cached_property import cached_property
from django import forms
from django.core.exceptions import ValidationError
from .fields import MultipleValuesField
from .utils import FilterSpec
MANY_LOOKUP_FIELD_OVERWRITES = {
'in': partial(MultipleValuesField, min_values=1),
'range': partial(MultipleValuesField, min_values=2, max_values=2),
}
LOOKUP_FIELD_OVERWRITES = {
'isnull': forms.BooleanField(),
'second': forms.IntegerField(min_value=0, max_value=59),
'minute': forms.IntegerField(min_value=0, max_value=59),
'hour': forms.IntegerField(min_value=0, max_value=23),
'week_day': forms.IntegerField(min_value=1, max_value=7),
'day': forms.IntegerField(min_value=1, max_value=31),
'month': forms.IntegerField(),
'year': forms.IntegerField(min_value=0, max_value=9999),
}
[docs]class Filter(object):
"""
Filter class which main job is to convert leaf ``LookupConfig``
to ``FilterSpec``.
Each filter by itself is meant to be used a "field" in the
``FilterSpec``.
Parameters
----------
source : str
Name of the attribute for which which filter applies to
within the model of the queryset to be filtered
as given to the ``FilterSet``.
form_field : Field
Instance of Django's ``forms.Field`` which will be used
to clean the filter value as provided in the queryset.
For example if field is ``IntegerField``, this filter
will make sure to convert the filtering value to integer
before creating a ``FilterSpec``.
lookups : list, optional
List of strings of allowed lookups for this filter.
By default all supported lookups are allowed.
default_lookup : str, optional
If the lookup is not provided in the querystring lookup key,
this lookup will be used. By default ``exact`` lookup is used.
For example the default lookup is used when querystring key is
``user__profile__email`` which is missing the lookup so ``exact``
will be used.
is_default : bool, optional
Boolean specifying if this filter should be used as a default
filter in the parent ``FilterSet``.
By default it is ``False``.
Primarily this is used when querystring lookup key
refers to a nested ``FilterSet`` however it does not specify
which filter to use. For example lookup key ``user__profile``
intends to filter something in the user's profile however
it does not specify by which field to filter on.
In that case the default filter within profile ``FilterSet``
will be used. At most, one default filter should be provided
in the ``FilterSet``.
Attributes
----------
parent : FilterSet
Parent ``FilterSet`` to which this filter is bound to
name : str
Name of the field as it is defined in parent ``FilterSet``
"""
def __init__(self, source=None, *args, **kwargs):
self._source = source
self.parent = None
self.name = None
self._init(*args, **kwargs)
def _init(self, form_field, lookups=None, default_lookup='exact', is_default=False):
self.form_field = form_field
self._given_lookups = lookups
self.default_lookup = default_lookup or self.default_lookup
self.is_default = is_default
[docs] def repr(self, prefix=''):
return (
'{name}('
'form_field={form_field}, '
'lookups={lookups}, '
'default_lookup="{default_lookup}", '
'is_default={is_default}'
')'
''.format(name=self.__class__.__name__,
form_field=self.form_field.__class__.__name__,
lookups=self._given_lookups or 'ALL',
default_lookup=self.default_lookup,
is_default=self.is_default)
)
def __repr__(self):
data = self.repr()
data = data if six.PY3 else data.encode('utf-8')
return data
@cached_property
def lookups(self):
if self._given_lookups:
return set(self._given_lookups)
if hasattr(self.root, 'filter_backend'):
return self.root.filter_backend.supported_lookups
return set()
@property
def source(self):
"""
Source field/attribute in queryset model to be used for filtering.
This property is helpful when ``source`` parameter is not provided
when instantiating ``Filter`` since it will use the filter name
as it is defined in the ``FilterSet``. For example::
>>> class MyFilterSet(FilterSet):
... foo = Filter(form_field=CharField())
... bar = Filter(source='stuff', form_field=CharField())
>>> fs = MyFilterSet()
>>> print(fs.fields['foo'].source)
foo
>>> print(fs.fields['bar'].source)
stuff
"""
return self._source or self.name
@property
def components(self):
"""
List of all components (source names) of all parent filtersets.
"""
if self.parent is None:
return []
return self.parent.components + [self.source]
[docs] def bind(self, name, parent):
"""
Bind the filter to the filterset.
This method should be used by the parent ``FilterSet``
since it allows to specify the parent and name of each
filter within the filterset.
"""
self.name = name
self.parent = parent
@property
def root(self):
"""
This gets the root filterset.
"""
if self.parent is None:
return self
return self.parent.root
[docs] def clean_value(self, value, lookup):
"""
Clean the filter value as appropriate for the given lookup.
Parameters
----------
value : str
Filter value as given in the querystring to be validated
and cleaned by using appropriate Django form field
lookup : str
Name of the lookup
See Also
--------
get_form_field
"""
form_field = self.get_form_field(lookup)
return form_field.clean(value)
[docs] def get_spec(self, config):
"""
Get the ``FilterSpec`` for the provided ``config``.
Parameters
----------
config : LookupConfig
Lookup configuration for which to build ``FilterSpec``.
The lookup should be a leaf configuration otherwise
``ValidationError`` is raised.
Returns
-------
FilterSpec
spec constructed from the given configuration.
"""
# lookup was explicitly provided
if isinstance(config.data, dict):
if not config.is_key_value():
raise ValidationError(
'Invalid filtering data provided. '
'Data is more complex then expected. '
'Most likely additional lookup was specified '
'after the final lookup (e.g. field__in__equal=value).'
)
lookup = config.name
value = config.value.data
# use default lookup
else:
lookup = self.default_lookup
value = config.data
if lookup not in self.lookups:
raise ValidationError('"{}" lookup is not supported'.format(lookup))
is_negated = '!' in config.key
value = self.clean_value(value, lookup)
return FilterSpec(self.components, lookup, value, is_negated)