# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
import enum
import re
from collections import defaultdict
from copy import deepcopy
import six
from cached_property import cached_property
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db.models.constants import LOOKUP_SEP
from django.http import QueryDict
from ..backends.django import DjangoFilterBackend
from ..exceptions import SkipFilter
from ..filters import Filter
from ..utils import LookupConfig
__all__ = ['FilterSet', 'FilterSetOptions', 'StrictMode']
[docs]class StrictMode(enum.Enum):
"""
Strictness mode enum.
:``drop`` (default):
ignores all filter failures. when any occur, ``FilterSet``
simply then does not filter provided queryset.
:``fail``:
when validation fails for any filter within ``FilterSet``,
all error are compiled and cumulative ``ValidationError`` is raised.
"""
drop = 'drop'
fail = 'fail'
LOOKUP_RE = re.compile(
r'^(?:[^\d\W]\w*)(?:{}?[^\d\W]\w*)*(?:\!)?$'
r''.format(LOOKUP_SEP), re.IGNORECASE
)
class FilterKeyValidator(RegexValidator):
regex = LOOKUP_RE
message = (
'Filter key is of invalid format. '
'It must be `name[__<relation>]*[__<method>][!]`.'
)
filter_key_validator = FilterKeyValidator()
[docs]class FilterSetOptions(object):
"""
Base class for handling options passed to ``FilterSet``
via ``Meta`` attribute.
"""
def __init__(self, options=None):
pass
class FilterSetMeta(type):
"""
Metaclass for creating ``FilterSet`` classes.
Its primary job is to do:
* collect all declared filters in all bases
and set them as ``_declared_filters`` on created
``FilterSet`` class.
* instantiate ``Meta`` by using ``filter_options_class`` attribute
"""
def __new__(cls, name, bases, attrs):
try:
parents = [b for b in bases if issubclass(b, FilterSet)]
except NameError:
parents = False
new_class = super(FilterSetMeta, cls).__new__(cls, name, bases, attrs)
# creating FilterSet itself
if not parents:
return new_class
filters = {}
for base in [vars(base) for base in bases] + [attrs]:
filters.update({k: v for k, v in base.items() if isinstance(v, Filter)})
new_class._declared_filters = filters
new_class.Meta = new_class.filter_options_class(
getattr(new_class, 'Meta', None)
)
return new_class
[docs]class FilterSet(six.with_metaclass(FilterSetMeta, Filter)):
"""
Main user-facing classes to use filtersets.
``FilterSet`` primarily does:
* takes queryset to filter
* takes querystring data which will be used to filter
given queryset
* from the querystring, it constructs a list of ``LookupConfig``
* loops over the created configs and attemps to get
``FilterSpec`` for each
* in the process, if delegates the job of constructing spec
to child filters when any match is found between filter
defined on the filter and name in the config
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``.
data : QueryDict, optional
QueryDict of querystring data.
Only optional when ``FilterSet`` is used a nested filter
within another ``FilterSet``.
queryset : iterable, optional
Can be any iterable as supported by the filter backend.
Only optional when ``FilterSet`` is used a nested filter
within another ``FilterSet``.
context : dict, optional
Context for filtering. This is passed to filtering backend.
Usually this would consist of passing ``request`` and ``view``
object from the Django view.
strict_mode : str, optional
Strict mode how ``FilterSet`` should behave when any validation
fails. See ``StrictMode`` doc for more information.
Default is ``drop``.
Attributes
----------
filter_backend_class
Class to be used as filter backend. By default
``DjangoFilterBackend`` is used.
filter_options_class
Class to be used to construct ``Meta`` during
``FilterSet`` class creation time in its metalclass.
"""
filter_backend_class = DjangoFilterBackend
filter_options_class = FilterSetOptions
def _init(self, data=None, queryset=None, context=None,
strict_mode=StrictMode.drop):
self.data = data
self.queryset = queryset
self.context = context or {}
self.strict_mode = strict_mode
[docs] def repr(self, prefix=''):
header = '{name}()'.format(name=self.__class__.__name__)
lines = [header] + [
'{prefix}{key} = {value}'.format(
prefix=prefix + ' ',
key=k,
value=v.repr(prefix=prefix + ' '),
)
for k, v in sorted(self.filters.items())
]
return '\n'.join(lines)
[docs] def get_filters(self):
"""
Get all filters defined in this filterset.
By default only declared filters are returned however
this methoc can be used a hook to customize that.
"""
return deepcopy(self._declared_filters)
@cached_property
def filters(self):
"""
Cached property for accessing filters available in this filteset.
In addition to getting filters via ``get_filters``,
this property binds all filters to the filtset by using ``bind``.
See Also
--------
get_filters
bind
"""
filters = self.get_filters()
for name, _filter in filters.items():
_filter.bind(name, self)
return filters
@cached_property
def default_filter(self):
"""
Cached property for looking up default filter.
Default filter is a filter which is defined with ``is_default=True``.
Useful when lookup config references nested filter without
specifying which field to filter. In that case default filter
will be used.
"""
return next(iter(filter(
lambda i: getattr(i, 'is_default', False),
self.filters.values()
)), None)
[docs] def validate_key(self, key):
"""
Validate that ``LookupConfig`` key is correct.
This is the key as provided in the querystring.
Currently key is validated against a regex expression.
"""
filter_key_validator(key)
[docs] def get_filter_backend(self):
"""
Get instantiated filter backend class.
This backend is then used to actually filter queryset.
"""
return self.filter_backend_class(
queryset=self.queryset,
context=self.context,
)
@cached_property
def filter_backend(self):
"""
Property for getting instantiated filter backend.
Primarily useful when accessing filter_backend outside
of the filterset such as leaf filters or integration
layers since backend has useful information for both of
those examples.
"""
assert self.data is not None, (
'Filter backend can only be used when data is provided '
'to filterset.'
)
return self.get_filter_backend()
[docs] def filter(self):
"""
Main method which should be used on root ``FilterSet``
to filter queryset.
This method:
* asserts that filtering is being done on root ``FilterSet``
and that all necessary data is provided
* creates ``LookupConfig``s from the provided data (querystring)
* loops over all configs and attemps to get ``FilterSpec``
for all of them
* instantiates filter backend
* uses the created filter specs to filter queryset by using specs
Returns
-------
querystring
Filtered queryset
"""
assert self.root is self, (
'``filter`` can only be called on root ``FilterSet``.'
)
assert self.queryset is not None, (
'``queryset`` was not passed for filtering.'
)
assert isinstance(self.data, QueryDict), (
'``data`` should be an instance of QueryDict.'
)
specs = self.get_specs()
self.filter_backend.bind(specs)
return self.filter_backend.filter()
[docs] def get_specs(self):
"""
Get ``FilterSpecs`` for the given querystring data.
This function does:
* unpacks the querystring data to ``LookupConfig``s
* loops throught all configs and uses appropriate children
filters to generate ``FilterSpec``s
* if any validations fails while generating specs,
all errors are collected and depending on ``strict_mode``
it reraises the errors or ignores them.
Returns
-------
list
List of ``FilterSpec``s
"""
configs = self._generate_lookup_configs()
specs = []
errors = defaultdict(list)
for data in configs:
try:
self.validate_key(data.key)
specs.append(self.get_spec(data))
except SkipFilter:
pass
except ValidationError as e:
errors[data.key].extend(
getattr(e, 'error_list', [getattr(e, 'message', '')])
)
if errors and self.strict_mode == StrictMode.fail:
raise ValidationError(dict(errors))
return specs
[docs] def get_spec(self, config):
"""
Get ``FilterSpec`` for the given ``LookupConfig``.
If the config is non leaf config (it has more nested fields),
then the appropriate matching child filter is used
to get the spec.
If the config however is a leaf config, then ``default_filter``
is used to get the spec, when available, and if not,
this filter is skipped.
Parameters
----------
config : LookupConfig
Config for which to generate ``FilterSpec``
Returns
-------
FilterSpec
Individual filter spec
"""
if isinstance(config.data, dict):
name, value = config.name, config.value
else:
if self.default_filter is None:
raise SkipFilter
name = self.default_filter.source
value = LookupConfig(config.key, config.data)
if name not in self.filters:
raise SkipFilter
return self.filters[name].get_spec(value)
def _generate_lookup_configs(self):
"""
Generate ``LookupConfig``s for all data in querystring data.
"""
for key, values in self.data.lists():
for value in values:
yield LookupConfig(key, six.moves.reduce(
lambda a, b: {b: a},
(key.replace('!', '').split(LOOKUP_SEP) + [value])[::-1]
))