Merge pull request #488 from carltongibson/develop

O.15 Release
This commit is contained in:
Carlton Gibson 2016-09-20 22:17:08 +02:00 committed by GitHub
commit a02db39fcb
47 changed files with 2503 additions and 730 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.14.0
current_version = 0.15.0
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?

View File

@ -11,17 +11,18 @@ python:
- "3.5"
env:
- DJANGO='https://github.com/django/django/archive/master.tar.gz'
- DJANGO='django>=1.10.0,<1.11.0'
- DJANGO='django>=1.9.0,<1.10.0'
- DJANGO='django>=1.8.0,<1.9.0'
- DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
- DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.9.0,<1.10.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.8.0,<1.9.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- DJANGO="django>=1.8.0,<1.9.0" REST_FRAMEWORK="djangorestframework>=3.3,<3.4"
install:
- travis_retry pip install $DJANGO
- travis_retry pip install -r requirements/travis-ci.txt
- travis_retry pip install -Ur requirements/test-ci.txt
- travis_retry pip install $DJANGO $REST_FRAMEWORK
script:
- coverage run --source django_filters runtests.py -v 2
- $W coverage run --source django_filters runtests.py -v 2
- coverage report
notifications:
@ -30,11 +31,15 @@ notifications:
matrix:
exclude:
- python: "3.3"
env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
env: DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
- python: "3.3"
env: DJANGO='django>=1.10.0,<1.11.0'
env: DJANGO="django>=1.10.0,<1.11.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
- python: "3.3"
env: DJANGO='django>=1.9.0,<1.10.0'
env: DJANGO="django>=1.9.0,<1.10.0" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
include:
- python: "3.5"
env: W='python -W error -m' DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
allow_failures:
- env: DJANGO='https://github.com/django/django/archive/master.tar.gz'
- env: W='python -W error -m' DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
- env: DJANGO='https://github.com/django/django/archive/master.tar.gz' REST_FRAMEWORK='djangorestframework>=3.4,<3.5'
fast_finish: true

View File

@ -1,3 +1,36 @@
Version 0.15.0 (2016-09-20)
---------------------------
This is a preparatory release for a 1.0. Lots of clean-up, lots of changes,
mostly backwards compatible.
Special thanks to Ryan P Kilby (@rpkilby) for lots of hard work.
Most changes should raise a Deprecation Warning.
**Note**: if you're doing *Clever Things™* with the various filter options
— ``filter_overrides`` etc — you may run into an `AttributeError` since these
are now defined on the metaclass and not on the filter itself.
(See the discussion on #459)
Summary: Highly Recommended, but take a moment to ensure everything still works.
* Added the DRF backend. #481
* Deprecated `MethodFilter` in favour of `Filter.method` #382
* Move filter options to metaclass #459
* Added `get_filter_predicate` hook. (Allows e.g. filtering on annotated fields) #469
* Rework Ordering options into a filter #472
* Hardened all deprecations for 1.0. Please do see the `Migration Notes`__
__ https://github.com/carltongibson/django-filter/blob/develop/docs/migration.txt
Version 0.14.0 (2016-08-14)
---------------------------
@ -15,6 +48,7 @@ Version 0.14.0 (2016-08-14)
* Various Minor Clean up issues.
Version 0.13.0 (2016-03-11)
---------------------------

View File

@ -14,6 +14,7 @@ Requirements
* Python 2.7, 3.3, 3.4, 3.5
* Django 1.8, 1.9, 1.10
* DRF 3.3 (Django 1.8 only), 3.4
Installation
------------

View File

@ -3,7 +3,7 @@ from __future__ import absolute_import
from .filterset import FilterSet
from .filters import *
__version__ = '0.14.0'
__version__ = '0.15.0'
def parse_version(version):

View File

@ -1,5 +1,15 @@
import django
from django.conf import settings
# django-crispy-forms is optional
try:
import crispy_forms
except ImportError:
crispy_forms = None
is_crispy = 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
def remote_field(field):
@ -22,3 +32,9 @@ def remote_queryset(field):
limit_choices_to = field.get_limit_choices_to()
return model._default_manager.complex_filter(limit_choices_to)
def format_value(widget, value):
if django.VERSION >= (1, 10):
return widget.format_value(value)
return widget._format_value(value)

106
django_filters/conf.py Normal file
View File

@ -0,0 +1,106 @@
from django.conf import settings as dj_settings
from django.core.signals import setting_changed
from django.utils.translation import ugettext_lazy as _
from .utils import deprecate
DEFAULTS = {
'HELP_TEXT_FILTER': True,
'HELP_TEXT_EXCLUDE': True,
'VERBOSE_LOOKUPS': {
# transforms don't need to be verbose, since their expressions are chained
'date': _('date'),
'year': _('year'),
'month': _('month'),
'day': _('day'),
'week_day': _('week day'),
'hour': _('hour'),
'minute': _('minute'),
'second': _('second'),
# standard lookups
'exact': _(''),
'iexact': _(''),
'contains': _('contains'),
'icontains': _('contains'),
'in': _('is in'),
'gt': _('is greater than'),
'gte': _('is greater than or equal to'),
'lt': _('is less than'),
'lte': _('is less than or equal to'),
'startswith': _('starts with'),
'istartswith': _('starts with'),
'endswith': _('ends with'),
'iendswith': _('ends with'),
'range': _('is in range'),
'isnull': _(''),
'regex': _('matches regex'),
'iregex': _('matches regex'),
'search': _('search'),
# postgres lookups
'contained_by': _('is contained by'),
'overlap': _('overlaps'),
'has_key': _('has key'),
'has_keys': _('has keys'),
'has_any_keys': _('has any keys'),
'trigram_similar': _('search'),
},
}
DEPRECATED_SETTINGS = [
'HELP_TEXT_FILTER',
'HELP_TEXT_EXCLUDE'
]
class Settings(object):
def __init__(self):
for setting in DEFAULTS:
value = self.get_setting(setting)
setattr(self, setting, value)
def VERBOSE_LOOKUPS():
"""
VERBOSE_LOOKUPS accepts a dictionary of {terms: verbose expressions}
or a zero-argument callable that returns a dictionary.
"""
def fget(self):
if callable(self._VERBOSE_LOOKUPS):
self._VERBOSE_LOOKUPS = self._VERBOSE_LOOKUPS()
return self._VERBOSE_LOOKUPS
def fset(self, value):
self._VERBOSE_LOOKUPS = value
return locals()
VERBOSE_LOOKUPS = property(**VERBOSE_LOOKUPS())
def get_setting(self, setting):
django_setting = 'FILTERS_%s' % setting
if setting in DEPRECATED_SETTINGS and hasattr(dj_settings, django_setting):
deprecate("The '%s' setting has been deprecated." % django_setting)
return getattr(dj_settings, django_setting, DEFAULTS[setting])
def change_setting(self, setting, value, enter, **kwargs):
if not setting.startswith('FILTERS_'):
return
setting = setting[8:] # strip 'FILTERS_'
# ensure a valid app setting is being overridden
if setting not in DEFAULTS:
return
# if exiting, refetch the value from settings.
value = value if enter else self.get_setting(setting)
setattr(self, setting, value)
settings = Settings()
setting_changed.connect(settings.change_setting)

View File

@ -2,21 +2,24 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import warnings
from collections import OrderedDict
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.itercompat import is_iterable
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from .conf import settings
from .fields import (
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
)
from .utils import deprecate, pretty_name
__all__ = [
@ -38,6 +41,7 @@ __all__ = [
'MultipleChoiceFilter',
'NumberFilter',
'NumericRangeFilter',
'OrderingFilter',
'RangeFilter',
'TimeFilter',
'TimeRangeFilter',
@ -49,6 +53,9 @@ __all__ = [
LOOKUP_TYPES = sorted(QUERY_TERMS)
EMPTY_VALUES = ([], (), {}, '', None)
def _lookup_type_warning():
warnings.warn('lookup_type is deprecated. Use lookup_expr instead.', DeprecationWarning, stacklevel=3)
@ -57,12 +64,14 @@ class Filter(object):
creation_counter = 0
field_class = forms.Field
def __init__(self, name=None, label=None, widget=None, action=None,
def __init__(self, name=None, label=None, widget=None, action=None, method=None,
lookup_expr='exact', required=False, distinct=False, exclude=False, **kwargs):
self.name = name
self.label = label
if action:
deprecate('Filter.action has been deprecated in favor of Filter.method')
self.filter = action
self.method = method
self.lookup_expr = lookup_expr
if 'lookup_type' in kwargs:
@ -84,6 +93,28 @@ class Filter(object):
"""
return qs.exclude if self.exclude else qs.filter
def method():
"""
Filter method needs to be lazily resolved, as it may be dependent on
the 'parent' FilterSet.
"""
def fget(self):
return self._method
def fset(self, value):
self._method = value
# clear existing FilterMethod
if isinstance(self.filter, FilterMethod):
del self.filter
# override filter w/ FilterMethod.
if value is not None:
self.filter = FilterMethod(self)
return locals()
method = property(**method())
def lookup_type():
def fget(self):
_lookup_type_warning()
@ -101,9 +132,9 @@ class Filter(object):
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):
if self.exclude and settings.HELP_TEXT_EXCLUDE:
help_text = _('This is an exclusion filter')
elif not self.exclude and getattr(settings, "FILTERS_HELP_TEXT_FILTER", True):
elif not self.exclude and settings.HELP_TEXT_FILTER:
help_text = _('Filter')
else:
help_text = ''
@ -144,7 +175,7 @@ class Filter(object):
value = value.value
else:
lookup = self.lookup_expr
if value in ([], (), {}, None, ''):
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
@ -174,38 +205,40 @@ class UUIDFilter(Filter):
class MultipleChoiceFilter(Filter):
"""
This filter preforms OR(by default) or AND(using conjoined=True) query
This filter performs OR(by default) or AND(using conjoined=True) query
on the selected options.
Advanced Use
------------
Advanced usage
--------------
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.
filtering may be a no-operation. 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.
You can override `get_filter_predicate` to use a custom filter.
By default it will use the filter's name for the key, and the value will
be the model object - or in case of passing in `to_field_name` the
value of that attribute on the model.
Override `is_noop` if you require a different test for your application.
Set `always_filter` to `False` after instantiation to enable the default
`is_noop` test. You can override `is_noop` if you need a different test
for your application.
`distinct` defaults to True on this class to preserve backward compatibility.
`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
kwargs.setdefault('distinct', True)
self.conjoined = kwargs.pop('conjoined', False)
super(MultipleChoiceFilter, self).__init__(*args, **kwargs)
def is_noop(self, qs, value):
"""
Return True to short-circuit unnecessary and potentially slow filtering.
Return `True` to short-circuit unnecessary and potentially slow
filtering.
"""
if self.always_filter:
return False
@ -217,26 +250,32 @@ class MultipleChoiceFilter(Filter):
return False
def filter(self, qs, value):
value = value or () # Make sure we have an iterable
if not value:
# Even though not a noop, no point filtering if empty.
return qs
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()
if not self.conjoined:
q = Q()
for v in set(value):
predicate = self.get_filter_predicate(v)
if self.conjoined:
qs = self.get_method(qs)(**{self.name: v})
qs = self.get_method(qs)(**predicate)
else:
q |= Q(**{self.name: v})
q |= Q(**predicate)
if self.distinct:
return self.get_method(qs)(q).distinct()
if not self.conjoined:
qs = self.get_method(qs)(q)
return self.get_method(qs)(q)
return qs.distinct() if self.distinct else qs
def get_filter_predicate(self, v):
try:
return {self.name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {self.name: v}
class DateFilter(Filter):
@ -453,11 +492,109 @@ class BaseRangeFilter(BaseCSVFilter):
super(BaseRangeFilter, self).__init__(*args, **kwargs)
class OrderingFilter(BaseCSVFilter, ChoiceFilter):
"""
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
two additional arguments that are used to build the ordering choices.
* ``fields`` is a mapping of {model field name: parameter name}. The
parameter names are exposed in the choices and mask/alias the field
names used in the ``order_by()`` call. Similar to field ``choices``,
``fields`` accepts the 'list of two-tuples' syntax that retains order.
``fields`` may also just be an iterable of strings. In this case, the
field names simply double as the exposed parameter names.
* ``field_labels`` is an optional argument that allows you to customize
the display label for the corresponding parameter. It accepts a mapping
of {field name: human readable label}. Keep in mind that the key is the
field name, and not the exposed parameter name.
Additionally, you can just provide your own ``choices`` if you require
explicit control over the exposed options. For example, when you might
want to disable descending sort options.
This filter is also CSV-based, and accepts multiple ordering params. The
default select widget does not enable the use of this, but it is useful
for APIs.
"""
descending_fmt = _('%s (descending)')
def __init__(self, *args, **kwargs):
"""
``fields`` may be either a mapping or an iterable.
``field_labels`` must be a map of field names to display labels
"""
fields = kwargs.pop('fields', {})
fields = self.normalize_fields(fields)
field_labels = kwargs.pop('field_labels', {})
self.param_map = {v: k for k, v in fields.items()}
if 'choices' not in kwargs:
kwargs['choices'] = self.build_choices(fields, field_labels)
kwargs.setdefault('label', _('Ordering'))
super(OrderingFilter, self).__init__(*args, **kwargs)
def get_ordering_value(self, param):
descending = param.startswith('-')
param = param[1:] if descending else param
field_name = self.param_map.get(param, param)
return "-%s" % field_name if descending else field_name
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
ordering = [self.get_ordering_value(param) for param in value]
return qs.order_by(*ordering)
@classmethod
def normalize_fields(cls, fields):
"""
Normalize the fields into an ordered map of {field name: param name}
"""
# fields is a mapping, copy into new OrderedDict
if isinstance(fields, dict):
return OrderedDict(fields)
# convert iterable of values => iterable of pairs (field name, param name)
assert is_iterable(fields), \
"'fields' must be an iterable (e.g., a list, tuple, or mapping)."
# fields is an iterable of field names
assert all(isinstance(field, six.string_types) or
is_iterable(field) and len(field) == 2 # may need to be wrapped in parens
for field in fields), \
"'fields' must contain strings or (field name, param name) pairs."
return OrderedDict([
(f, f) if isinstance(f, six.string_types) else f for f in fields
])
def build_choices(self, fields, labels):
ascending = [
(param, labels.get(field, pretty_name(param)))
for field, param in fields.items()
]
descending = [
('-%s' % pair[0], self.descending_fmt % pair[1])
for pair in ascending
]
# interleave the ascending and descending choices
return [val for pair in zip(ascending, descending) for val in pair]
class MethodFilter(Filter):
"""
This filter will allow you to run a method that exists on the filterset class
"""
def __init__(self, *args, **kwargs):
deprecate('MethodFilter has been deprecated in favor of Filter.method')
# Get the action out of the kwargs
action = kwargs.get('action', None)
@ -492,3 +629,43 @@ class MethodFilter(Filter):
if parent_filter_method is not None:
return parent_filter_method(qs, value)
return qs
class FilterMethod(object):
"""
This helper is used to override Filter.filter() when a 'method' argument
is passed. It proxies the call to the actual method on the filter's parent.
"""
def __init__(self, filter_instance):
self.f = filter_instance
def __call__(self, qs, value):
if value in EMPTY_VALUES:
return qs
return self.method(qs, self.f.name, value)
@property
def method(self):
"""
Resolve the method on the parent filterset.
"""
instance = self.f
# noop if 'method' is a function
if callable(instance.method):
return instance.method
# otherwise, method is the name of a method on the parent FilterSet.
assert hasattr(instance, 'parent'), \
"Filter '%s' must have a parent FilterSet to find '.%s()'" % \
(instance.name, instance.method)
parent = instance.parent
method = getattr(parent, instance.method, None)
assert callable(method), \
"Expected parent FilterSet '%s.%s' to have a '.%s()' method." % \
(parent.__class__.__module__, parent.__class__.__name__, instance.method)
return method

View File

@ -2,12 +2,10 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import copy
import re
from collections import OrderedDict
from django import forms
from django.forms.forms import NON_FIELD_ERRORS
from django.core.validators import EMPTY_VALUES
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.related import ForeignObjectRel
@ -15,12 +13,12 @@ from django.utils import six
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
from .compat import remote_field, remote_model, remote_queryset
from .compat import remote_field, remote_queryset
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter,
DurationFilter)
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field, deprecate
DurationFilter, OrderingFilter)
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field, pretty_name, deprecate
ORDER_BY_FIELD = 'o'
@ -151,7 +149,18 @@ class FilterSetOptions(object):
self.fields = getattr(options, 'fields', None)
self.exclude = getattr(options, 'exclude', None)
self.filter_overrides = getattr(options, 'filter_overrides', {})
if hasattr(options, 'order_by'):
deprecate('Meta.order_by has been deprecated.', 1)
if hasattr(options, 'order_by_field'):
deprecate('Meta.order_by_field has been deprecated.', 1)
self.order_by = getattr(options, 'order_by', False)
self.order_by_field = getattr(options, 'order_by_field', ORDER_BY_FIELD)
self.strict = getattr(options, 'strict', STRICTNESS.RETURN_NO_RESULTS)
self.form = getattr(options, 'form', forms.Form)
@ -175,6 +184,26 @@ class FilterSetMetaclass(type):
opts = new_class._meta = FilterSetOptions(
getattr(new_class, 'Meta', None))
if hasattr(new_class, 'strict'):
deprecate('strict has been deprecated. Use Meta.strict instead.')
new_class._meta.strict = new_class.strict
if hasattr(new_class, 'order_by_field'):
deprecate('order_by_field has been moved to the Meta class.')
new_class._meta.order_by_field = new_class.order_by_field
if hasattr(new_class, 'filter_overrides'):
deprecate('filter_overrides has been moved to the Meta class.')
new_class._meta.filter_overrides = new_class.filter_overrides
assert not hasattr(new_class, 'get_order_by'), \
'get_order_by() has been deprecated. Subclass OrderingFilter and override .filter() instead. ' \
'See: https://django-filter.readthedocs.io/en/latest/migration.html"'
assert not hasattr(new_class, 'get_ordering_field'), \
'get_ordering_field() has been deprecated. Use OrderingFilter instead. ' \
'See: https://django-filter.readthedocs.io/en/latest/migration.html"'
# TODO: replace with deprecations
# if opts.model and opts.fields:
if opts.model:
@ -188,36 +217,40 @@ class FilterSetMetaclass(type):
raise TypeError("Meta.fields contains a field that isn't defined "
"on this FilterSet: {}".format(not_defined))
# TODO: remove with deprecations
# check key existence instead of setdefault - prevents unnecessary filter construction
order_by_field = new_class._meta.order_by_field
if opts.order_by and order_by_field not in filters:
filters[order_by_field] = new_class.get_ordering_filter(opts, filters)
new_class.declared_filters = declared_filters
new_class.base_filters = filters
return new_class
FILTER_FOR_DBFIELD_DEFAULTS = {
models.AutoField: {
'filter_class': NumberFilter
},
models.CharField: {
'filter_class': CharFilter
},
models.TextField: {
'filter_class': CharFilter
},
models.BooleanField: {
'filter_class': BooleanFilter
},
models.DateField: {
'filter_class': DateFilter
},
models.DateTimeField: {
'filter_class': DateTimeFilter
},
models.TimeField: {
'filter_class': TimeFilter
},
models.DurationField: {
'filter_class': DurationFilter
},
models.AutoField: {'filter_class': NumberFilter},
models.CharField: {'filter_class': CharFilter},
models.TextField: {'filter_class': CharFilter},
models.BooleanField: {'filter_class': BooleanFilter},
models.DateField: {'filter_class': DateFilter},
models.DateTimeField: {'filter_class': DateTimeFilter},
models.TimeField: {'filter_class': TimeFilter},
models.DurationField: {'filter_class': DurationFilter},
models.DecimalField: {'filter_class': NumberFilter},
models.SmallIntegerField: {'filter_class': NumberFilter},
models.IntegerField: {'filter_class': NumberFilter},
models.PositiveIntegerField: {'filter_class': NumberFilter},
models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
models.FloatField: {'filter_class': NumberFilter},
models.NullBooleanField: {'filter_class': BooleanFilter},
models.SlugField: {'filter_class': CharFilter},
models.EmailField: {'filter_class': CharFilter},
models.FilePathField: {'filter_class': CharFilter},
models.URLField: {'filter_class': CharFilter},
models.GenericIPAddressField: {'filter_class': CharFilter},
models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
models.UUIDField: {'filter_class': UUIDFilter},
models.OneToOneField: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
@ -238,56 +271,11 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
'queryset': remote_queryset(f),
}
},
models.DecimalField: {
'filter_class': NumberFilter,
},
models.SmallIntegerField: {
'filter_class': NumberFilter,
},
models.IntegerField: {
'filter_class': NumberFilter,
},
models.PositiveIntegerField: {
'filter_class': NumberFilter,
},
models.PositiveSmallIntegerField: {
'filter_class': NumberFilter,
},
models.FloatField: {
'filter_class': NumberFilter,
},
models.NullBooleanField: {
'filter_class': BooleanFilter,
},
models.SlugField: {
'filter_class': CharFilter,
},
models.EmailField: {
'filter_class': CharFilter,
},
models.FilePathField: {
'filter_class': CharFilter,
},
models.URLField: {
'filter_class': CharFilter,
},
models.GenericIPAddressField: {
'filter_class': CharFilter,
},
models.CommaSeparatedIntegerField: {
'filter_class': CharFilter,
},
models.UUIDField: {
'filter_class': UUIDFilter,
},
}
class BaseFilterSet(object):
filter_overrides = {}
order_by_field = ORDER_BY_FIELD
# What to do on on validation errors
strict = STRICTNESS.RETURN_NO_RESULTS
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
def __init__(self, data=None, queryset=None, prefix=None, strict=None):
self.is_bound = data is not None
@ -296,8 +284,9 @@ class BaseFilterSet(object):
queryset = self._meta.model._default_manager.all()
self.queryset = queryset
self.form_prefix = prefix
if strict is not None:
self.strict = strict
# What to do on on validation errors
self.strict = self._meta.strict if strict is None else strict
self.filters = copy.deepcopy(self.base_filters)
# propagate the model being used through the filters
@ -359,23 +348,6 @@ class BaseFilterSet(object):
if value is not None: # valid & clean data
qs = filter_.filter(qs, value)
if self._meta.order_by:
order_field = self.form.fields[self.order_by_field]
data = self.form[self.order_by_field].data
ordered_value = None
try:
ordered_value = order_field.clean(data)
except forms.ValidationError:
pass
# With a None-queryset, ordering must be enforced (#84).
if (ordered_value in EMPTY_VALUES and
self.strict == STRICTNESS.RETURN_NO_RESULTS):
ordered_value = self.form.fields[self.order_by_field].choices[0][0]
if ordered_value:
qs = qs.order_by(*self.get_order_by(ordered_value))
self._qs = qs
return self._qs
@ -386,7 +358,7 @@ class BaseFilterSet(object):
fields = OrderedDict([
(name, filter_.field)
for name, filter_ in six.iteritems(self.filters)])
fields[self.order_by_field] = self.ordering_field
Form = type(str('%sForm' % self.__class__.__name__),
(self._meta.form,), fields)
if self._meta.together:
@ -397,49 +369,48 @@ class BaseFilterSet(object):
self._form = Form(prefix=self.form_prefix)
return self._form
def get_ordering_field(self):
if self._meta.order_by:
if isinstance(self._meta.order_by, (list, tuple)):
if isinstance(self._meta.order_by[0], (list, tuple)):
# e.g. (('field', 'Display name'), ...)
choices = [(f[0], f[1]) for f in self._meta.order_by]
else:
choices = []
for f in self._meta.order_by:
if f[0] == '-':
label = _('%s (descending)' % capfirst(f[1:]))
else:
label = capfirst(f)
choices.append((f, label))
@classmethod
def get_ordering_filter(cls, opts, filters):
assert not isinstance(opts.fields, dict), \
"'order_by' is not compatible with the 'fields' dict syntax. Use OrderingFilter instead."
def display_text(name, fltr):
"""
``name`` is the filter's attribute name on the FilterSet
``text`` is the current display text, which is either the ``name``
or an explicitly assigned label.
"""
# TODO: use `fltr._label` in label-improvements branch
text = fltr.label or name.lstrip('-')
if name.startswith('-'):
text = _('%s (descending)' % text)
return pretty_name(text)
if isinstance(opts.order_by, (list, tuple)):
# e.g. (('field', 'Display name'), ...)
if isinstance(opts.order_by[0], (list, tuple)):
choices = [(f[0], f[1]) for f in opts.order_by]
fields = {filters.get(f[0].lstrip('-')).name: f[0] for f in opts.order_by}
return OrderingFilter(choices=choices, fields=fields)
# e.g. ('field1', 'field2', ...)
else:
# add asc and desc field names
# use the filter's label if provided
choices = []
for f, fltr in self.filters.items():
choices.extend([
(f, fltr.label or capfirst(f)),
("-%s" % (f), _('%s (descending)' % (fltr.label or capfirst(f))))
])
return forms.ChoiceField(label=_("Ordering"), required=False,
choices=choices)
# (filter name, filter instance)
order_by = [(f, filters.get(f.lstrip('-'))) for f in opts.order_by]
@property
def ordering_field(self):
if not hasattr(self, '_ordering_field'):
self._ordering_field = self.get_ordering_field()
return self._ordering_field
# preference filter label over attribute name
choices = [(f, display_text(f, fltr)) for f, fltr in order_by]
fields = {fltr.name: f for f, fltr in order_by}
return OrderingFilter(choices=choices, fields=fields)
def get_order_by(self, order_choice):
re_ordering_field = re.compile(r'(?P<inverse>\-?)(?P<field>.*)')
m = re.match(re_ordering_field, order_choice)
inverted = m.group('inverse')
filter_api_name = m.group('field')
# opts.order_by = True
order_by = filters.items()
_filter = self.filters.get(filter_api_name, None)
if _filter and filter_api_name != _filter.name:
return [inverted + _filter.name]
return [order_choice]
fields = [(fltr.name, f) for f, fltr in order_by]
labels = {f: display_text(f, fltr) for f, fltr in order_by}
return OrderingFilter(fields=fields, field_labels=labels)
@classmethod
def filters_for_model(cls, model, opts):
@ -447,7 +418,7 @@ class BaseFilterSet(object):
fields = opts.fields
if fields is None:
DEFAULTS = dict(FILTER_FOR_DBFIELD_DEFAULTS)
DEFAULTS.update(cls.filter_overrides)
DEFAULTS.update(opts.filter_overrides)
fields = get_all_model_fields(model, field_types=DEFAULTS.keys())
return filters_for_model(
@ -493,8 +464,9 @@ class BaseFilterSet(object):
@classmethod
def filter_for_lookup(cls, f, lookup_type):
DEFAULTS = dict(FILTER_FOR_DBFIELD_DEFAULTS)
DEFAULTS.update(cls.filter_overrides)
DEFAULTS = dict(cls.FILTER_DEFAULTS)
if hasattr(cls, '_meta'):
DEFAULTS.update(cls._meta.filter_overrides)
data = try_dbfield(DEFAULTS.get, f.__class__) or {}
filter_class = data.get('filter_class')

View File

@ -0,0 +1,5 @@
# flake8: noqa
from __future__ import absolute_import
from .backends import DjangoFilterBackend
from .filterset import FilterSet
from .filters import *

View File

@ -0,0 +1,64 @@
from __future__ import absolute_import
from django.template import loader
from rest_framework.filters import BaseFilterBackend
from .. import compat
from . import filterset
if compat.is_crispy:
filter_template = 'django_filters/rest_framework/crispy_form.html'
else:
filter_template = 'django_filters/rest_framework/form.html'
class DjangoFilterBackend(BaseFilterBackend):
default_filter_set = filterset.FilterSet
template = filter_template
def get_filter_class(self, view, queryset=None):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)
if filter_class:
filter_model = filter_class.Meta.model
assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
return filter_class
if filter_fields:
class AutoFilterSet(self.default_filter_set):
class Meta:
model = queryset.model
fields = filter_fields
return AutoFilterSet
return None
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset).qs
return queryset
def to_html(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if not filter_class:
return None
filter_instance = filter_class(request.query_params, queryset=queryset)
context = {
'filter': filter_instance
}
template = loader.get_template(self.template)
return compat.template_render(template, context)

View File

@ -0,0 +1,10 @@
from ..filters import *
from ..widgets import BooleanWidget
class BooleanFilter(BooleanFilter):
def __init__(self, *args, **kwargs):
kwargs.setdefault('widget', BooleanWidget)
super(BooleanFilter, self).__init__(*args, **kwargs)

View File

@ -0,0 +1,39 @@
from __future__ import absolute_import
from copy import deepcopy
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_filters import filterset
from .filters import BooleanFilter, IsoDateTimeFilter
from .. import compat
if compat.is_crispy:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
FILTER_FOR_DBFIELD_DEFAULTS.update({
models.DateTimeField: {'filter_class': IsoDateTimeFilter},
models.BooleanField: {'filter_class': BooleanFilter},
})
class FilterSet(filterset.FilterSet):
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
if compat.is_crispy:
layout_components = list(self.form.fields.keys()) + [
Submit('', _('Submit'), css_class='btn-default'),
]
helper = FormHelper()
helper.form_method = 'GET'
helper.template_pack = 'bootstrap3'
helper.layout = Layout(*layout_components)
self.form.helper = helper

View File

@ -0,0 +1,5 @@
{% load crispy_forms_tags %}
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}

View File

@ -0,0 +1,6 @@
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
<form class="form" action="" method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>

View File

@ -9,6 +9,11 @@ from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from django.utils import six, timezone
try:
from django.forms.utils import pretty_name
except ImportError: # Django 1.8
from django.forms.forms import pretty_name
from .compat import remote_field, remote_model
from .exceptions import FieldLookupError

View File

@ -16,6 +16,8 @@ from django.utils.safestring import mark_safe
from django.utils.six import string_types
from django.utils.translation import ugettext as _
from .compat import format_value
class LinkWidget(forms.Widget):
def __init__(self, attrs=None, choices=()):
@ -68,9 +70,9 @@ class LinkWidget(forms.Widget):
except AttributeError:
url = urlencode(data)
return self.option_string() % {
'attrs': selected and ' class="selected"' or '',
'query_string': url,
'label': force_text(option_label)
'attrs': selected and ' class="selected"' or '',
'query_string': url,
'label': force_text(option_label)
}
def option_string(self):
@ -151,7 +153,7 @@ class CSVWidget(forms.TextInput):
def render(self, name, value, attrs=None):
if self._isiterable(value):
value = [force_text(self._format_value(v)) for v in value]
value = [force_text(format_value(self, v)) for v in value]
value = ','.join(list(value))
return super(CSVWidget, self).render(name, value, attrs)

View File

@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
# built documents.
#
# The short X.Y version.
version = '0.14.0'
version = '0.15.0'
# The full version, including alpha/beta/rc tags.
release = '0.14.0'
release = '0.15.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

BIN
docs/img/form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -14,6 +14,7 @@ Contents:
install
usage
rest_framework
ref/filterset
ref/filters
ref/fields

View File

@ -3,6 +3,8 @@ Installing django-filter
Install with pip:
.. code-block:: bash
pip install django-filter
And then add ``'django_filters'`` to your ``INSTALLED_APPS``.

View File

@ -8,6 +8,42 @@ release has also been created to help with migration. It is compatible with
both the existing and new APIs and will raise warnings for deprecated behavior.
MethodFilter and Filter.action replaced by Filter.method
--------------------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/382
The functionality of ``MethodFilter`` and ``Filter.action`` has been merged
together and replaced by the ``Filter.method`` parameter. The ``method``
parameter takes either a callable or the name of a ``FilterSet`` method. The
signature now takes an additional ``name`` argument that is the name of the
model field to be filtered on.
Since ``method`` is now a parameter of all filters, inputs are validated and
cleaned by its ``field_class``. The function will receive the cleaned value
instead of the raw value.
.. code-block:: python
# 0.x
class UserFilter(FilterSet):
last_login = filters.MethodFilter()
def filter_last_login(self, qs, value):
# try to convert value to datetime, which may fail.
if value and looks_like_a_date(value):
value = datetime(value)
return qs.filter(last_login=value})
# 1.0
class UserFilter(FilterSet):
last_login = filters.CharFilter(method='filter_last_login')
def filter_last_login(self, qs, name, value):
return qs.filter(**{name: value})
QuerySet methods are no longer proxied
--------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/440
@ -52,3 +88,78 @@ the ``Meta.exclude`` attribute.
class Meta:
model = User
exclude = ['password']
Move FilterSet options to Meta class
------------------------------------
Details: https://github.com/carltongibson/django-filter/issues/430
Several ``FilterSet`` options have been moved to the ``Meta`` class to prevent
potential conflicts with declared filter names. This includes:
* ``filter_overrides``
* ``strict``
* ``order_by_field``
.. code-block:: python
# 0.x
class UserFilter(FilterSet):
filter_overrides = {}
strict = STRICTNESS.RAISE_VALIDATION_ERROR
order_by_field = 'order'
...
# 1.0
class UserFilter(FilterSet):
...
class Meta:
filter_overrides = {}
strict = STRICTNESS.RAISE_VALIDATION_ERROR
order_by_field = 'order'
FilterSet ordering replaced by OrderingFilter
---------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/472
The FilterSet ordering options and methods have been deprecated and replaced
by :ref:`OrderingFilter <ordering-filter>`. Deprecated options include:
* ``Meta.order_by``
* ``Meta.order_by_field``
These options retain backwards compatibility with the following caveats:
* ``order_by`` asserts that ``Meta.fields`` is not using the dict syntax. This
previously was undefined behavior, however the migration code is unable to
support it.
* Prior, if no ordering was specified in the request, the FilterSet implicitly
filtered by the first param in the ``order_by`` option. This behavior cannot
be easily emulated but can be fixed by ensuring that the passed in queryset
explicitly calls ``.order_by()``.
.. code-block:: python
filterset = MyFilterSet(queryset=MyModel.objects.order_by('field'))
The following methods are deprecated and will raise an assertion if present
on the FilterSet:
* ``.get_order_by()``
* ``.get_ordering_field()``
To fix this, simply remove the methods from your class. You can subclass
``OrderingFilter`` to migrate any custom logic.
Deprecated ``FILTERS_HELP_TEXT_FILTER`` and ``FILTERS_HELP_TEXT_EXCLUDE``
-------------------------------------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/437
Generated filter labels in 1.0 will be more descriptive, including humanized
text about the lookup being performed and if the filter is an exclusion filter.
These settings will no longer have an effect and will be removed in the 1.0 release.

View File

@ -43,13 +43,51 @@ additional ones that django-filter provides which may be useful:
* :ref:`RangeWidget <range-widget>` -- this widget is used with ``RangeFilter``
to generate two form input elements using a single field.
``action``
.. _filter-method:
``method``
~~~~~~~~~~
An optional callable that tells the filter how to handle the queryset. It
recieves a ``QuerySet`` and the value to filter on and should return a
``Queryset`` that is filtered appropriately. `action` will default to
``filter_{value-of-name-attribute}``
An optional argument that tells the filter how to handle the queryset. It can
accept either a callable or the name of a method on the ``FilterSet``. The
method receives a ``QuerySet``, the name of the model field to filter on, and
the value to filter with. It should return a ``Queryset`` that is filtered
appropriately.
The passed in value is validated and cleaned by the filter's ``field_class``,
so raw value transformation and empty value checking should be unnecessary.
.. code-block:: python
class F(FilterSet):
"""Filter for Books by if books are published or not"""
published = BooleanFilter(name='published_on', method='filter_published')
def filter_published(self, queryset, name, value):
# construct the full lookup expression.
lookup = '__'.join([name, 'isnull'])
return queryset.filter(**{lookup: False})
# alternatively, it may not be necessary to construct the lookup.
return queryset.filter(published_on__isnull=False)
class Meta:
model = Book
fields = ['published']
# Callables may also be defined out of the class scope.
def filter_not_empty(queryset, name, value):
lookup = '__'.join([name, 'isnull'])
return queryset.filter(**{lookup: False})
class F(FilterSet):
"""Filter for Books by if books are published or not"""
published = BooleanFilter(name='published_on', method=filter_not_empty)
class Meta:
model = Book
fields = ['published']
``lookup_expr``
~~~~~~~~~~~~~~~
@ -114,13 +152,21 @@ Any additional keyword arguments are stored as the ``extra`` parameter on the fi
ModelChoiceFilter and ModelMultipleChoiceFilter arguments
---------------------------------------------------------
These arguments apply specifically to ModelChoiceFiler and
These arguments apply specifically to ModelChoiceFilter and
ModelMultipleChoiceFilter only.
``queryset``
~~~~~~~~~~~~
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to operate on which must be passed as a kwarg.
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to
operate on which must be passed as a kwarg.
``to_field_name``
~~~~~~~~~~~~~~~~~
If you pass in ``to_field_name`` (which gets forwarded to the Django field),
it will be used also in the default ``get_filter_predicate`` implementation
as the model's attribute.
Filters
@ -236,7 +282,7 @@ Matches on a date and time. Used with ``DateTimeField`` by default.
Uses ``IsoDateTimeField`` to support filtering on ISO 8601 formatted dates, as are often used in APIs, and are employed by default by Django REST Framework.
Example. ::
Example::
class F(FilterSet):
"""Filter for Books by date published, using ISO 8601 formatted dates"""
@ -263,10 +309,11 @@ and week designators, e.g. 'P3DT10H22M').
Similar to a ``ChoiceFilter`` except it works with related models, used for
``ForeignKey`` by default.
If automatically instantiated ``ModelChoiceFilter`` will use the default ``QuerySet`` for the
related field. If manually instantiated you **must** provide the ``queryset`` kwarg.
If automatically instantiated, ``ModelChoiceFilter`` will use the default
``QuerySet`` for the related field. If manually instantiated you **must**
provide the ``queryset`` kwarg.
Example. ::
Example::
class F(FilterSet):
"""Filter for books by author"""
@ -282,9 +329,55 @@ Example. ::
Similar to a ``MultipleChoiceFilter`` except it works with related models, used
for ``ManyToManyField`` by default.
As with ``ModelChoiceFilter``, if automatically instantiated ``ModelMultipleChoiceFilter`` will use
the default ``QuerySet`` for the related field. If manually instantiated you **must** provide the
``queryset`` kwarg.
As with ``ModelChoiceFilter``, if automatically instantiated,
``ModelMultipleChoiceFilter`` will use the default ``QuerySet`` for the related
field. If manually instantiated you **must** provide the ``queryset`` kwarg.
To use a custom field name for the lookup, you can use ``to_field_name``::
class FooFilter(BaseFilterSet):
foo = django_filters.filters.ModelMultipleChoiceFilter(
name='attr__uuid',
to_field_name='uuid',
queryset=Foo.objects.all(),
)
If you want to use a custom queryset, e.g. to add annotated fields, this can be
done as follows::
class MyMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
def get_filter_predicate(self, v):
return {'annotated_field': v.annotated_field}
def filter(self, qs, value):
if value:
qs = qs.annotate_with_custom_field()
qs = super().filter(qs, value)
return qs
foo = MyMultipleChoiceFilter(
to_field_name='annotated_field',
queryset=Model.objects.annotate_with_custom_field(),
)
The ``annotate_with_custom_field`` method would be defined through a custom
QuerySet, which then gets used as the model's manager::
class CustomQuerySet(models.QuerySet):
def annotate_with_custom_field(self):
return self.annotate(
custom_field=Case(
When(foo__isnull=False,
then=F('foo__uuid')),
When(bar__isnull=False,
then=F('bar__uuid')),
default=None,
),
)
class MyModel(models.Model):
objects = CustomQuerySet.as_manager()
``NumberFilter``
~~~~~~~~~~~~~~~~
@ -478,7 +571,7 @@ this filter class is used in conjunction with another filter class, as this
class **only** validates that the incoming value is comma-separated. The secondary
filter is then used to validate the individual values.
Example. ::
Example::
class NumberInFilter(BaseInFilter, NumericFilter):
pass
@ -505,7 +598,7 @@ This is a base class used for creating RANGE lookup filters. It behaves
identically to ``BaseInFilter`` with the exception that it expects only two
comma-separated values.
Example. ::
Example::
class NumberRangeFilter(BaseInFilter, NumericFilter):
pass
@ -533,7 +626,7 @@ This is a ``Filter`` that will allow you to run a method that exists on the filt
this filter is a property of. Set the `action` to a string that will map to a method on the
filter set class. `action` will default to ``filter_{value-of-name-attribute}``
Example. ::
Example::
class F(FilterSet):
"""Filter for Books by if books are published or not"""
@ -547,3 +640,81 @@ Example. ::
class Meta:
model = Book
fields = ['published']
.. _ordering-filter:
``OrderingFilter``
~~~~~~~~~~~~~~~~~~
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
two additional arguments that are used to build the ordering choices.
* ``fields`` is a mapping of {model field name: parameter name}. The
parameter names are exposed in the choices and mask/alias the field
names used in the ``order_by()`` call. Similar to field ``choices``,
``fields`` accepts the 'list of two-tuples' syntax that retains order.
``fields`` may also just be an iterable of strings. In this case, the
field names simply double as the exposed parameter names.
* ``field_labels`` is an optional argument that allows you to customize
the display label for the corresponding parameter. It accepts a mapping
of {field name: human readable label}. Keep in mind that the key is the
field name, and not the exposed parameter name.
.. code-block:: python
class UserFilter(FilterSet):
account = CharFilter(name='username')
status = NumberFilter(name='status')
o = OrderingFilter(
# tuple-mapping retains order
fields=(
('username', 'account'),
('first_name', 'first_name'),
('last_name', 'last_name'),
},
# labels do not need to retain order
field_labels={
'username': 'User account',
}
)
class Meta:
model = User
fields = ['first_name', 'last_name']
>>> UserFilter().filter['o'].field.choices
[
('account', 'User account'),
('-account', 'User account (descending)'),
('first_name', 'First name'),
('-first_name', 'First name (descending)'),
('last_name', 'Last name'),
('-last_name', 'Last name (descending)'),
]
Additionally, you can just provide your own ``choices`` if you require
explicit control over the exposed options. For example, when you might
want to disable descending sort options.
.. code-block:: python
class UserFilter(FilterSet):
account = CharFilter(name='username')
status = NumberFilter(name='status')
o = OrderingFilter(
choices=(
('account', 'Account'),
),
fields={
'username': 'account',
},
)
This filter is also CSV-based, and accepts multiple ordering params. The
default select widget does not enable the use of this, but it is useful
for APIs.

View File

@ -16,9 +16,13 @@ Default: ``True``
Enable / disable the default field ``help_text`` on filters where ``exclude=True`` (:doc:`/ref/filters`). This does not affect fields which explicitly define ``help_text``.
.. note:: This setting is pending deprecation.
FILTERS_HELP_TEXT_FILTER
------------------------
Default: ``True``
Enable / disable the default field ``help_text`` on filters where ``exclude=False`` (:doc:`/ref/filters`). This does not affect fields which explicitly define ``help_text``.
.. note:: This setting is pending deprecation.

111
docs/rest_framework.txt Normal file
View File

@ -0,0 +1,111 @@
Django Rest Framework
=====================
Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package.
__ http://www.django-rest-framework.org/
__ http://www.django-rest-framework.org/api-guide/filtering/
Quickstart
----------
Using the new ``FilterSet`` simply requires changing the import path. Instead of importing from ``django_filters``, import from the ``rest_framework`` sub-package.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
...
Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_backends``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('category', 'in_stock')
If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting.
.. code-block:: python
# settings.py
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
...
),
}
Adding a FilterSet with ``filter_class``
----------------------------------------
To enable filtering with a ``FilterSet``, add it to the ``filter_class`` parameter on your view class.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductFilter(filters.FilterSet):
min_price = django_filters.NumberFilter(name="price", lookup_expr='gte')
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = ProductFilter
Specifying ``filter_fields``
----------------------------
You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields <fields>`.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('category', 'in_stock')
# Equivalent FilterSet:
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
Crispy Forms
------------
If you are using DRF's browsable API or admin API you may also want to install `django-crispy-forms`, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. Note that this isn't actively supported, although pull requests for bug fixes are welcome.
.. code-block:: bash
pip install django-crispy-forms
With crispy forms installed and added to Django's `INSTALLED_APPS`, the browsable API will present a filtering control for `DjangoFilterBackend`, like so:
.. image:: img/form.png

View File

@ -18,6 +18,8 @@ Set up a virtualenv for the test suite
Run the following to create a new virtualenv to run the test suite in::
.. code-block:: bash
virtualenv django-filter-tests
cd django-filter-tests
. bin/activate
@ -27,10 +29,14 @@ Get a copy of django-filter
Get the django-filter source code using the following command::
.. code-block:: bash
git clone https://github.com/alex/django-filter.git
Switch to the django-filter directory::
.. code-block:: bash
cd django-filter
Install the test dependencies
@ -39,10 +45,14 @@ Install the test dependencies
Run the following to install the test dependencies within the
virutalenv::
.. code-block:: bash
pip install -r requirements/test.txt
Run the django-filter tests::
.. code-block:: bash
python runtests.py
@ -52,5 +62,7 @@ Testing all supported versions
You can also use the excellent tox testing tool to run the tests against all supported versions of
Python and Django. Install tox globally, and then simply run::
.. code-block:: bash
tox

View File

@ -236,71 +236,25 @@ default filters for all the models fields of the same kind using
}
Custom filtering with MethodFilter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Customize filtering with ``Filter.method``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want fine control over each individual filter attribute, you can use
the ``MethodFilter`` filter.
You can control the behavior of a filter by specifying a ``method`` to perform
filtering. View more information in the :ref:`method reference <filter-method>`.
By passing in the name of a custom defined filter function as an ``action``,
the filter attribute gets linked to the custom filter function.
Here is an example of overriding the filter function of the
filter attribute ``username``
::
.. code-block:: python
class F(django_filters.FilterSet):
username = MethodFilter(action='my_custom_filter')
username = CharFilter(method='my_custom_filter')
class Meta:
model = User
fields = ['username']
class Meta:
model = User
fields = ['username']
def my_custom_filter(self, queryset, value):
return queryset.filter(
username=value
)
The filter function can also be defined outside of the filter class scope.
Though you would need to pass in the actual function value, not it's name.
::
def my_custom_filter(queryset, value):
return queryset.filter(
username=value
)
class F(django_filters.FilterSet):
# Notice: In this case, action accepts a func, not a string
username = MethodFilter(action=my_custom_filter)
class Meta:
model = User
fields = ['username']
Lastly, when using a ``MethodFilter``, there is no need to define an action.
You may simply do the following and ``filter_username`` will be auto-detected
and used. ::
class F(FilterSet):
username = MethodFilter()
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username__contains='ke'
)
Under the hood, if ``action`` is not defined, ``django_filter``
searches for a class method with a name that follows the pattern
``filter_{{ATTRIBUTE_NAME}}``. For example, if the attribute name is
``email``, then the filter class will be scanned for the filter function
``filter_email``. If no action is provided, and no filter class
function is found, then the filter attribute will be left unfiltered.
def my_custom_filter(self, queryset, name, value):
return queryset.filter(**{
name: value,
})
The view

View File

@ -13,13 +13,14 @@ pathtools==0.1.2
pbr==1.7.0
pkginfo==1.2.1
Pygments==2.1.3
pytz==2015.7
pytz==2016.6.1
PyYAML==3.11
requests==2.9.1
requests-toolbelt==0.6.0
six==1.9.0
snowballstemmer==1.2.1
Sphinx==1.3.6
sphinx-autobuild==0.6.0
sphinx-rtd-theme==0.1.9
tornado==4.2.1
twine==1.6.5

3
requirements/test-ci.txt Normal file
View File

@ -0,0 +1,3 @@
coverage
mock
pytz

View File

@ -1,5 +1,3 @@
-r maintainer.txt
coverage==3.7.1
django-discover-runner
mock
sphinx-autobuild
-r travis-ci.txt
django
djangorestframework

View File

@ -1 +0,0 @@
-r test.txt

View File

@ -6,7 +6,7 @@ f = open('README.rst')
readme = f.read()
f.close()
version = '0.14.0'
version = '0.15.0'
if sys.argv[-1] == 'publish':
if os.system("pip freeze | grep wheel"):
@ -34,7 +34,7 @@ setup(
maintainer_email='carlton.gibson@noumenal.es',
url='http://github.com/carltongibson/django-filter/tree/master',
packages=find_packages(exclude=['tests']),
package_data = {
package_data={
'django_filters': [
'locale/*/LC_MESSAGES/*',
],

View File

View File

@ -0,0 +1,28 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
class BasicModel(models.Model):
text = models.CharField(
max_length=100,
verbose_name=_("Text comes here"),
help_text=_("Text description.")
)
class BaseFilterableItem(models.Model):
text = models.CharField(max_length=100)
class FilterableItem(BaseFilterableItem):
decimal = models.DecimalField(max_digits=4, decimal_places=2)
date = models.DateField()
class DjangoFilterOrderingModel(models.Model):
date = models.DateField()
text = models.CharField(max_length=10)
class Meta:
ordering = ['-date']

View File

@ -0,0 +1,378 @@
from __future__ import unicode_literals
import datetime
from decimal import Decimal
from django.conf.urls import url
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.dateparse import parse_date
try:
from django.urls import reverse
except ImportError:
# Django < 1.10 compatibility
from django.core.urlresolvers import reverse
from rest_framework import generics, serializers, status
from rest_framework.test import APIRequestFactory
from django_filters import filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from .models import BaseFilterableItem, BasicModel, FilterableItem, DjangoFilterOrderingModel
factory = APIRequestFactory()
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
fields = '__all__'
# Basic filter on a list view.
class FilterFieldsRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date']
filter_backends = (DjangoFilterBackend,)
# These class are used to test a filter class.
class SeveralFieldsFilter(FilterSet):
text = filters.CharFilter(lookup_expr='icontains')
decimal = filters.NumberFilter(lookup_expr='lt')
date = filters.DateFilter(lookup_expr='gt')
class Meta:
model = FilterableItem
fields = ['text', 'decimal', 'date']
class FilterClassRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
# These classes are used to test a misconfigured filter class.
class MisconfiguredFilter(FilterSet):
text = filters.CharFilter(lookup_expr='icontains')
class Meta:
model = BasicModel
fields = ['text']
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = MisconfiguredFilter
filter_backends = (DjangoFilterBackend,)
class FilterClassDetailView(generics.RetrieveAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
# These classes are used to test base model filter support
class BaseFilterableItemFilter(FilterSet):
text = filters.CharFilter()
class Meta:
model = BaseFilterableItem
fields = '__all__'
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = BaseFilterableItemFilter
filter_backends = (DjangoFilterBackend,)
# Regression test for #814
class FilterFieldsQuerysetView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date']
filter_backends = (DjangoFilterBackend,)
class GetQuerysetView(generics.ListCreateAPIView):
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
def get_queryset(self):
return FilterableItem.objects.all()
urlpatterns = [
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
url(r'^get-queryset/$', GetQuerysetView.as_view(), name='get-queryset-view'),
]
class CommonFilteringTestCase(TestCase):
def _serialize_object(self, obj):
return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
def setUp(self):
"""
Create 10 FilterableItem instances.
"""
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
for i in range(10):
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
decimal = base_data[1] + i
date = base_data[2] - datetime.timedelta(days=i * 2)
FilterableItem(text=text, decimal=decimal, date=date).save()
self.objects = FilterableItem.objects
self.data = [
self._serialize_object(obj)
for obj in self.objects.all()
]
class IntegrationTestFiltering(CommonFilteringTestCase):
"""
Integration tests for filtered list views.
"""
def test_get_filtered_fields_root_view(self):
"""
GET requests to paginated ListCreateAPIView should return paginated results.
"""
view = FilterFieldsRootView.as_view()
# Basic test with no filter.
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
# Tests that the decimal filter works.
search_decimal = Decimal('2.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter works.
search_date = datetime.date(2012, 9, 22)
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
self.assertEqual(response.data, expected_data)
def test_filter_with_queryset(self):
"""
Regression test for #814.
"""
view = FilterFieldsQuerysetView.as_view()
# Tests that the decimal filter works.
search_decimal = Decimal('2.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
def test_filter_with_get_queryset_only(self):
"""
Regression test for #834.
"""
view = GetQuerysetView.as_view()
request = factory.get('/get-queryset/')
view(request).render()
# Used to raise "issubclass() arg 2 must be a class or tuple of classes"
# here when neither `model' nor `queryset' was specified.
def test_get_filtered_class_root_view(self):
"""
GET requests to filtered ListCreateAPIView that have a filter_class set
should return filtered results.
"""
view = FilterClassRootView.as_view()
# Basic test with no filter.
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
# Tests that the decimal filter set with 'lt' in the filter class works.
search_decimal = Decimal('4.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter set with 'gt' in the filter class works.
search_date = datetime.date(2012, 10, 2)
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
self.assertEqual(response.data, expected_data)
# Tests that the text filter set with 'icontains' in the filter class works.
search_text = 'ff'
request = factory.get('/', {'text': '%s' % search_text})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if search_text in f['text'].lower()]
self.assertEqual(response.data, expected_data)
# Tests that multiple filters works.
search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2)
request = factory.get('/', {
'decimal': '%s' % (search_decimal,),
'date': '%s' % (search_date,)
})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
def test_incorrectly_configured_filter(self):
"""
An error should be displayed when the filter class is misconfigured.
"""
view = IncorrectlyConfiguredRootView.as_view()
request = factory.get('/')
self.assertRaises(AssertionError, view, request)
def test_base_model_filter(self):
"""
The `get_filter_class` model checks should allow base model filters.
"""
view = BaseFilterableItemFilterRootView.as_view()
request = factory.get('/?text=aaa')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_unknown_filter(self):
"""
GET requests with filters that aren't configured should return 200.
"""
view = FilterFieldsRootView.as_view()
search_integer = 10
request = factory.get('/', {'integer': '%s' % search_integer})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@override_settings(ROOT_URLCONF='tests.rest_framework.test_backends')
class IntegrationTestDetailFiltering(CommonFilteringTestCase):
"""
Integration tests for filtered detail views.
"""
def _get_url(self, item):
return reverse('detail-view', kwargs=dict(pk=item.pk))
def test_get_filtered_detail_view(self):
"""
GET requests to filtered RetrieveAPIView that have a filter_class set
should return filtered results.
"""
item = self.objects.all()[0]
data = self._serialize_object(item)
# Basic test with no filter.
response = self.client.get(self._get_url(item))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, data)
# Tests that the decimal filter set that should fail.
search_decimal = Decimal('4.25')
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
response = self.client.get(
'{url}'.format(url=self._get_url(high_item)),
{'decimal': '{param}'.format(param=search_decimal)})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Tests that the decimal filter set that should succeed.
search_decimal = Decimal('4.25')
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
low_item_data = self._serialize_object(low_item)
response = self.client.get(
'{url}'.format(url=self._get_url(low_item)),
{'decimal': '{param}'.format(param=search_decimal)})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, low_item_data)
# Tests that multiple filters works.
search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2)
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
valid_item_data = self._serialize_object(valid_item)
response = self.client.get(
'{url}'.format(url=self._get_url(valid_item)), {
'decimal': '{decimal}'.format(decimal=search_decimal),
'date': '{date}'.format(date=search_date)
})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, valid_item_data)
class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
class Meta:
model = DjangoFilterOrderingModel
fields = '__all__'
class DjangoFilterOrderingTests(TestCase):
def setUp(self):
data = [{
'date': datetime.date(2012, 10, 8),
'text': 'abc'
}, {
'date': datetime.date(2013, 10, 8),
'text': 'bcd'
}, {
'date': datetime.date(2014, 10, 8),
'text': 'cde'
}]
for d in data:
DjangoFilterOrderingModel.objects.create(**d)
def test_default_ordering(self):
class DjangoFilterOrderingView(generics.ListAPIView):
serializer_class = DjangoFilterOrderingSerializer
queryset = DjangoFilterOrderingModel.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ['text']
ordering = ('-date',)
view = DjangoFilterOrderingView.as_view()
request = factory.get('/')
response = view(request)
self.assertEqual(
response.data,
[
{'id': 3, 'date': '2014-10-08', 'text': 'cde'},
{'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
{'id': 1, 'date': '2012-10-08', 'text': 'abc'}
]
)

View File

@ -0,0 +1,15 @@
from django.test import TestCase
from django_filters.rest_framework import filters
from django_filters.widgets import BooleanWidget
class BooleanFilterTests(TestCase):
def test_widget(self):
# Ensure that `BooleanFilter` uses the correct widget when importing
# from `rest_framework.filters`.
f = filters.BooleanFilter()
self.assertEqual(f.widget, BooleanWidget)

View File

@ -0,0 +1,22 @@
from django.test import TestCase
from django_filters.rest_framework import FilterSet, filters
from django_filters.widgets import BooleanWidget
from ..models import User, Article
class FilterSetFilterForFieldTests(TestCase):
def test_isodatetimefilter(self):
field = Article._meta.get_field('published')
result = FilterSet.filter_for_field(field, 'published')
self.assertIsInstance(result, filters.IsoDateTimeFilter)
self.assertEqual(result.name, 'published')
def test_booleanfilter_widget(self):
field = User._meta.get_field('is_active')
result = FilterSet.filter_for_field(field, 'is_active')
self.assertIsInstance(result, filters.BooleanFilter)
self.assertEqual(result.widget, BooleanWidget)

View File

@ -10,6 +10,7 @@ INSTALLED_APPS = (
'django.contrib.auth',
'django_filters',
'tests',
'tests.rest_framework',
)
ROOT_URLCONF = 'tests.urls'
@ -24,6 +25,10 @@ TEMPLATES = [{
}]
SILENCED_SYSTEM_CHECKS = [
'1_7.W001', # Unset MIDDLEWARE_CLASSES warning
]
MIDDLEWARE = []
# help verify that DEFAULTS is importable from conf.
def FILTERS_VERBOSE_LOOKUPS():
from django_filters.conf import DEFAULTS
return DEFAULTS['VERBOSE_LOOKUPS']

64
tests/test_conf.py Normal file
View File

@ -0,0 +1,64 @@
from django.test import TestCase, override_settings
from django_filters.conf import settings
class DefaultSettingsTests(TestCase):
def test_verbose_loookups(self):
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
self.assertIn('exact', settings.VERBOSE_LOOKUPS)
def test_help_text_filter(self):
self.assertTrue(settings.HELP_TEXT_FILTER)
def test_help_text_exclude(self):
self.assertTrue(settings.HELP_TEXT_EXCLUDE)
class OverrideSettingsTests(TestCase):
def test_attribute_override(self):
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
original = settings.VERBOSE_LOOKUPS
with override_settings(FILTERS_VERBOSE_LOOKUPS=None):
self.assertIsNone(settings.VERBOSE_LOOKUPS)
self.assertIs(settings.VERBOSE_LOOKUPS, original)
def test_missing_attribute_override(self):
# ensure that changed setting behaves correctly when
# not originally present in the user's settings.
from django.conf import settings as dj_settings
self.assertFalse(hasattr(dj_settings, 'FILTERS_HELP_TEXT_FILTER'))
# Default value
self.assertTrue(settings.HELP_TEXT_FILTER)
with override_settings(FILTERS_HELP_TEXT_FILTER=None):
self.assertIsNone(settings.HELP_TEXT_FILTER)
# Revert to default
self.assertTrue(settings.HELP_TEXT_FILTER)
def test_non_filters_setting(self):
self.assertFalse(hasattr(settings, 'USE_TZ'))
with override_settings(USE_TZ=False):
self.assertFalse(hasattr(settings, 'USE_TZ'))
self.assertFalse(hasattr(settings, 'USE_TZ'))
def test_non_existent_setting(self):
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
with override_settings(FILTERS_FOOBAR='blah'):
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))

View File

@ -1,14 +1,29 @@
import functools
import warnings
from django.test import TestCase
import mock
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from django_filters import FilterSet
from django_filters.filters import CharFilter
from django_filters.conf import Settings
from django_filters.filters import Filter, CharFilter, MethodFilter
from django_filters.filterset import STRICTNESS
from .models import User
from .models import NetworkSetting
from .models import SubnetMaskField
def silence(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
f(*args, **kwargs)
return wrapped
class UserFilter(FilterSet):
class Meta:
model = User
@ -53,6 +68,177 @@ class FilterSetContainerDeprecationTests(TestCase):
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
class MethodFilterDeprecationTests(TestCase):
def test_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
username = MethodFilter()
class Meta:
model = User
fields = []
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
# old tests
@silence
def test_filtering(self):
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter(action='filter_username')
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username=value
)
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'alex'}).qs),
[User.objects.get(username='alex')])
self.assertEqual(list(F({'username': 'jose'}).qs),
list())
@silence
def test_filtering_external(self):
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
def filter_username(queryset, value):
return queryset.filter(
username=value
)
class F(FilterSet):
username = MethodFilter(action=filter_username)
class Meta:
model = User
fields = ['username']
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'alex'}).qs),
[User.objects.get(username='alex')])
self.assertEqual(list(F({'username': 'jose'}).qs),
list())
@silence
def test_filtering_default_attribute_action(self):
User.objects.create(username='mike')
User.objects.create(username='jake')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter()
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username__contains='ke'
)
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'mike'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')],)
self.assertEqual(list(F({'username': 'jake'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')])
self.assertEqual(list(F({'username': 'aaron'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')])
@silence
def test_filtering_default(self):
User.objects.create(username='mike')
User.objects.create(username='jake')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter()
email = MethodFilter()
class Meta:
model = User
fields = ['username']
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'mike'}).qs),
list(User.objects.all()))
self.assertEqual(list(F({'username': 'jake'}).qs),
list(User.objects.all()))
self.assertEqual(list(F({'username': 'aaron'}).qs),
list(User.objects.all()))
class FilterActionDeprecationTests(TestCase):
def test_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
username = CharFilter(action=lambda x: x)
class Meta:
model = User
fields = []
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_filter_using_action(self):
qs = mock.NonCallableMock(spec=[])
action = mock.Mock(spec=['filter'])
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
f = Filter(action=action)
result = f.filter(qs, 'value')
action.assert_called_once_with(qs, 'value')
self.assertNotEqual(qs, result)
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_filtering_with_action(self):
User.objects.create(username='alex', status=1)
User.objects.create(username='jacob', status=2)
User.objects.create(username='aaron', status=2)
User.objects.create(username='carl', status=0)
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
class F(FilterSet):
username = CharFilter(action=lambda qs, value: (
qs.filter(**{'username__startswith': value})
))
class Meta:
model = User
fields = ['username']
f = F({'username': 'a'}, queryset=User.objects.all())
self.assertQuerysetEqual(
f.qs, ['alex', 'aaron'], lambda o: o.username, False)
class FilterSetMetaDeprecationTests(TestCase):
def test_fields_not_set(self):
with warnings.catch_warnings(record=True) as w:
@ -95,14 +281,438 @@ class FilterSetMetaDeprecationTests(TestCase):
warnings.simplefilter("always")
class F(FilterSet):
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter}
}
class Meta:
model = NetworkSetting
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter},
}
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
self.assertIn("Not setting Meta.fields with Meta.model is undocumented behavior", str(w[-1].message))
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
class StrictnessDeprecationTests(TestCase):
def test_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
strict = False
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_passthrough(self):
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
class F(FilterSet):
strict = False
self.assertEqual(F._meta.strict, False)
class FilterOverridesDeprecationTests(TestCase):
def test_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
filter_overrides = {}
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_passthrough(self):
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
class F(FilterSet):
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter},
}
class Meta:
model = NetworkSetting
fields = '__all__'
self.assertDictEqual(F._meta.filter_overrides, {
SubnetMaskField: {'filter_class': CharFilter},
})
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
class OrderByFieldDeprecationTests(TestCase):
def test_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
order_by_field = 'field'
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_passthrough(self):
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
class F(FilterSet):
order_by_field = 'field'
self.assertEqual(F._meta.order_by_field, 'field')
class OrderByDeprecationTests(TestCase):
def test_order_by_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
class Meta:
order_by = True
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_order_by_field_notification(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
class Meta:
order_by_field = 'field'
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
def test_get_order_by_assertion(self):
with self.assertRaises(AssertionError):
class F(FilterSet):
def get_order_by(self):
pass
def test_get_ordering_field_assertion(self):
with self.assertRaises(AssertionError):
class F(FilterSet):
def get_ordering_field(self):
pass
class DeprecatedOrderingFilterSetTests(TestCase):
def setUp(self):
self.alex = User.objects.create(username='alex', status=1)
self.jacob = User.objects.create(username='jacob', status=2)
self.aaron = User.objects.create(username='aaron', status=2)
self.carl = User.objects.create(username='carl', status=0)
self.qs = User.objects.all().order_by('id')
# old filterset tests
@silence
def test_ordering(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['username', 'status']
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
f = F({'o': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
@silence
def test_ordering_on_unknown_value(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, [], lambda o: o.username)
@silence
def test_ordering_on_unknown_value_results_in_default_ordering_without_strict(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
strict = STRICTNESS.IGNORE
self.assertFalse(F._meta.strict)
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
@silence
def test_ordering_on_unknown_value_results_in_default_ordering_with_strict_raise(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F({'o': 'username'}, queryset=self.qs)
with self.assertRaises(ValidationError) as excinfo:
f.qs.all()
self.assertEqual(excinfo.exception.message_dict,
{'o': ['Select a valid choice. username is not one '
'of the available choices.']})
# No default order_by should get applied.
f = F({}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
@silence
def test_ordering_on_different_field(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
f = F({'o': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
@silence
def test_ordering_uses_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F({'o': 'account'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
@silence
def test_reverted_ordering_uses_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F({'o': '-account'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
@silence
def test_ordering_with_overridden_field_name(self):
"""
Set the `order_by_field` on the filterset and ensure that the
field name is respected.
"""
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
order_by_field = 'order'
f = F({'order': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
@silence
def test_ordering_descending_set(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['username', '-username']
f = F({'o': '-username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
@silence
def test_ordering_descending_unset(self):
""" Test ordering descending works when order_by=True. """
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F({'o': '-username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
class DeprecatedOrderingFormTests(TestCase):
@silence
def test_ordering(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F().form
self.assertEqual(len(f.fields), 3)
self.assertIn('o', f.fields)
self.assertEqual(f.fields['o'].choices, [('status', 'Status')])
@silence
def test_ordering_uses_all_fields(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices, [
('username', 'Username'),
('-username', 'Username (descending)'),
('status', 'Status'),
('-status', 'Status (descending)')])
@silence
def test_ordering_uses_filter_label(self):
class F(FilterSet):
username = CharFilter(label='Account')
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices, [
('username', 'Account'),
('-username', 'Account (descending)'),
('status', 'Status'),
('-status', 'Status (descending)')])
@silence
def test_ordering_uses_explicit_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices, [
('account', 'Account'),
('-account', 'Account (descending)'),
('status', 'Status'),
('-status', 'Status (descending)')])
@silence
def test_ordering_with_overridden_field_name(self):
"""
Set the `order_by_field` on the filterset and ensure that the
field name is respected.
"""
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
order_by_field = 'order'
f = F().form
self.assertNotIn('o', f.fields)
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices, [('status', 'Status')])
@silence
def test_ordering_with_overridden_field_name_and_descending(self):
"""
Set the `order_by_field` on the filterset and ensure that the
field name is respected.
"""
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status', '-status']
order_by_field = 'order'
f = F().form
self.assertNotIn('o', f.fields)
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices, [('status', 'Status'), ('-status', 'Status (descending)')])
@silence
def test_ordering_with_overridden_field_name_and_using_all_fields(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
order_by_field = 'order'
f = F().form
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices, [
('username', 'Username'),
('-username', 'Username (descending)'),
('status', 'Status'),
('-status', 'Status (descending)')])
@silence
def test_ordering_with_custom_display_names(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = [('status', 'Current status')]
f = F().form
self.assertEqual(
f.fields['o'].choices, [('status', 'Current status')])
class DeprecatedSettingsTests(TestCase):
def test_filter_help_text(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with override_settings(FILTERS_HELP_TEXT_FILTER=False):
Settings()
self.assertEqual(len(w), 1)
self.assertIn("The 'FILTERS_HELP_TEXT_FILTER' setting has been deprecated.", str(w[0].message))
def test_exclude_help_text(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
with override_settings(FILTERS_HELP_TEXT_EXCLUDE=False):
Settings()
self.assertEqual(len(w), 1)
self.assertIn("The 'FILTERS_HELP_TEXT_EXCLUDE' setting has been deprecated.", str(w[0].message))

View File

@ -111,26 +111,6 @@ class LookupTypeFieldTests(TestCase):
f.clean(['12.34', 'lt']),
Lookup(to_d(12.34), 'lt'))
@unittest.skipIf(django.VERSION >= (1, 6),
'Django 1.6 uses html5 fields')
def test_render(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
self.assertHTMLEqual(f.widget.render('price', ''), """
<input type="text" name="price_0" />
<select name="price_1">
<option value="gt">gt</option>
<option value="lt">lt</option>
</select>""")
self.assertHTMLEqual(f.widget.render('price', ['abc', 'lt']), """
<input type="text" name="price_0" value="abc" />
<select name="price_1">
<option value="gt">gt</option>
<option selected="selected" value="lt">lt</option>
</select>""")
@unittest.skipUnless(django.VERSION >= (1, 6),
'Django 1.6 uses html5 fields')
def test_render_used_html5(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])

View File

@ -21,7 +21,6 @@ from django_filters.filters import DateFromToRangeFilter
from django_filters.filters import DateTimeFromToRangeFilter
# from django_filters.filters import DateTimeFilter
from django_filters.filters import DurationFilter
from django_filters.filters import MethodFilter
from django_filters.filters import MultipleChoiceFilter
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import NumberFilter
@ -153,7 +152,6 @@ class ChoiceFilterTests(TestCase):
f = F({'status': '0'})
self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username, False)
def test_filtering_on_explicitly_defined_field(self):
"""
Test for #30.
@ -167,6 +165,7 @@ class ChoiceFilterTests(TestCase):
class F(FilterSet):
status = ChoiceFilter(choices=STATUS_CHOICES)
class Meta:
model = User
fields = ['status']
@ -186,7 +185,6 @@ class ChoiceFilterTests(TestCase):
self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username, False)
class MultipleChoiceFilterTests(TestCase):
def test_filtering(self):
@ -857,11 +855,11 @@ class AllValuesFilterTests(TestCase):
class F(FilterSet):
username = AllValuesFilter()
strict = False
class Meta:
model = User
fields = ['username']
strict = False
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'alex'}).qs),
@ -893,24 +891,23 @@ class AllValuesMultipleFilterTests(TestCase):
list())
class MethodFilterTests(TestCase):
class FilterMethodTests(TestCase):
def setUp(self):
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
def test_filtering(self):
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter(action='filter_username')
username = CharFilter(method='filter_username')
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username=value
)
def filter_username(self, queryset, name, value):
return queryset.filter(**{name: value})
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'alex'}).qs),
@ -918,18 +915,12 @@ class MethodFilterTests(TestCase):
self.assertEqual(list(F({'username': 'jose'}).qs),
list())
def test_filtering_external(self):
User.objects.create(username='alex')
User.objects.create(username='jacob')
User.objects.create(username='aaron')
def filter_username(queryset, value):
return queryset.filter(
username=value
)
def test_filtering_callable(self):
def filter_username(queryset, name, value):
return queryset.filter(**{name: value})
class F(FilterSet):
username = MethodFilter(action=filter_username)
username = CharFilter(method=filter_username)
class Meta:
model = User
@ -942,57 +933,6 @@ class MethodFilterTests(TestCase):
list())
def test_filtering_default_attribute_action(self):
User.objects.create(username='mike')
User.objects.create(username='jake')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter()
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username__contains='ke'
)
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'mike'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')],)
self.assertEqual(list(F({'username': 'jake'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')])
self.assertEqual(list(F({'username': 'aaron'}).qs),
[User.objects.get(username='mike'),
User.objects.get(username='jake')])
def test_filtering_default(self):
User.objects.create(username='mike')
User.objects.create(username='jake')
User.objects.create(username='aaron')
class F(FilterSet):
username = MethodFilter()
email = MethodFilter()
class Meta:
model = User
fields = ['username']
self.assertEqual(list(F().qs), list(User.objects.all()))
self.assertEqual(list(F({'username': 'mike'}).qs),
list(User.objects.all()))
self.assertEqual(list(F({'username': 'jake'}).qs),
list(User.objects.all()))
self.assertEqual(list(F({'username': 'aaron'}).qs),
list(User.objects.all()))
class O2ORelationshipTests(TestCase):
def setUp(self):
@ -1682,19 +1622,6 @@ class MiscFilterSetTests(TestCase):
f = F({'username': 'alex', 'status': '2'}, queryset=qs)
self.assertQuerysetEqual(f.qs, [], lambda o: o.pk)
def test_filter_with_action(self):
class F(FilterSet):
username = CharFilter(action=lambda qs, value: (
qs.filter(**{'username__startswith': value})))
class Meta:
model = User
fields = ['username']
f = F({'username': 'a'}, queryset=User.objects.all())
self.assertQuerysetEqual(
f.qs, ['alex', 'aaron'], lambda o: o.username, False)
def test_filter_with_initial(self):
class F(FilterSet):
status = ChoiceFilter(choices=STATUS_CHOICES, initial=1)

View File

@ -1,10 +1,10 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from collections import OrderedDict
from datetime import date, time, timedelta, datetime
import mock
import warnings
import unittest
from django import forms
from django.test import TestCase, override_settings
@ -42,6 +42,7 @@ from django_filters.filters import (
BaseInFilter,
BaseRangeFilter,
UUIDFilter,
OrderingFilter,
LOOKUP_TYPES)
from tests.models import Book, User
@ -112,29 +113,32 @@ class FilterTests(TestCase):
def test_field_params(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
spec=['__call__']) as mocked:
f = Filter(name='somefield', label='somelabel',
widget='somewidget')
widget='somewidget')
f.field
mocked.assert_called_once_with(required=False,
label='somelabel', widget='somewidget', help_text=mock.ANY)
label='somelabel',
widget='somewidget',
help_text=mock.ANY)
def test_field_extra_params(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
spec=['__call__']) as mocked:
f = Filter(someattr='someattr')
f.field
mocked.assert_called_once_with(required=mock.ANY,
label=mock.ANY, widget=mock.ANY, help_text=mock.ANY,
someattr='someattr')
label=mock.ANY, widget=mock.ANY,
help_text=mock.ANY,
someattr='someattr')
def test_field_with_required_filter(self):
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
spec=['__call__']) as mocked:
f = Filter(required=True)
f.field
mocked.assert_called_once_with(required=True,
label=mock.ANY, widget=mock.ANY, help_text=mock.ANY)
mocked.assert_called_once_with(required=True, label=mock.ANY,
widget=mock.ANY, help_text=mock.ANY)
def test_filtering(self):
qs = mock.Mock(spec=['filter'])
@ -193,12 +197,12 @@ class FilterTests(TestCase):
qs.filter.assert_called_once_with(somefield__exact='value')
self.assertNotEqual(qs, result)
def test_filter_using_action(self):
def test_filter_using_method(self):
qs = mock.NonCallableMock(spec=[])
action = mock.Mock(spec=['filter'])
f = Filter(action=action)
method = mock.Mock()
f = Filter(method=method)
result = f.filter(qs, 'value')
action.assert_called_once_with(qs, 'value')
method.assert_called_once_with(qs, None, 'value')
self.assertNotEqual(qs, result)
def test_filtering_uses_distinct(self):
@ -436,7 +440,7 @@ class MultipleChoiceFilterTests(TestCase):
[5, ]),
((books[3].pk, books[4].pk),
[]),
)
)
users = User.objects.all()
for item in filter_list:
@ -511,6 +515,21 @@ class ModelMultipleChoiceFilterTests(TestCase):
self.assertIsInstance(field, forms.ModelMultipleChoiceField)
self.assertEqual(field.queryset, qs)
def test_filtering_to_field_name(self):
qs = User.objects.all()
f = ModelMultipleChoiceFilter(name='first_name',
to_field_name='first_name',
queryset=qs)
user = User.objects.create(first_name='Firstname')
self.assertEqual(f.get_filter_predicate(user),
{'first_name': 'Firstname'})
self.assertEqual(f.get_filter_predicate('FilterValue'),
{'first_name': 'FilterValue'})
self.assertEqual(list(f.filter(qs, ['Firstname'])), [user])
self.assertEqual(list(f.filter(qs, [user])), [user])
class NumberFilterTests(TestCase):
@ -858,7 +877,8 @@ class AllValuesFilterTests(TestCase):
def test_default_field_with_assigning_model(self):
mocked = mock.Mock()
chained_call = '.'.join(['_default_manager', 'distinct.return_value',
'order_by.return_value', 'values_list.return_value'])
'order_by.return_value',
'values_list.return_value'])
mocked.configure_mock(**{chained_call: iter([])})
f = AllValuesFilter()
f.model = mocked
@ -976,3 +996,104 @@ class BaseRangeFilterTests(TestCase):
f = NumberInFilter()
f.filter(qs, [1, 2])
qs.filter.assert_called_once_with(None__range=[1, 2])
class OrderingFilterTests(TestCase):
def test_default_field(self):
f = OrderingFilter()
field = f.field
self.assertIsInstance(field, forms.ChoiceField)
def test_filtering(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter()
f.filter(qs, ['a', 'b'])
qs.order_by.assert_called_once_with('a', 'b')
def test_filtering_descending(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter()
f.filter(qs, ['-a'])
qs.order_by.assert_called_once_with('-a')
def test_filtering_with_fields(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter(fields={'a': 'b'})
f.filter(qs, ['b', '-b'])
qs.order_by.assert_called_once_with('a', '-a')
def test_filtering_skipped_with_none_value(self):
qs = mock.Mock(spec=['order_by'])
f = OrderingFilter()
result = f.filter(qs, None)
self.assertEqual(qs, result)
def test_choices_unaltered(self):
# provided 'choices' should not be altered when 'fields' is present
f = OrderingFilter(
choices=(('a', 'A'), ('b', 'B')),
fields=(('a', 'c'), ('b', 'd')),
)
self.assertSequenceEqual(f.field.choices, (
('a', 'A'),
('b', 'B'),
))
def test_choices_from_fields(self):
f = OrderingFilter(
fields=(('a', 'c'), ('b', 'd')),
)
self.assertSequenceEqual(f.field.choices, (
('c', 'C'),
('-c', 'C (descending)'),
('d', 'D'),
('-d', 'D (descending)'),
))
def test_field_labels(self):
f = OrderingFilter(
fields=(('a', 'c'), ('b', 'd')),
field_labels={'a': 'foo'},
)
self.assertSequenceEqual(f.field.choices, (
('c', 'foo'),
('-c', 'foo (descending)'),
('d', 'D'),
('-d', 'D (descending)'),
))
def test_normalize_fields(self):
f = OrderingFilter.normalize_fields
O = OrderedDict
self.assertIn('a', f({'a': 'b'}))
self.assertEqual(
f(O([('a', 'b'), ('c', 'd')])),
O([('a', 'b'), ('c', 'd')])
)
self.assertEqual(
f([('a', 'b'), ('c', 'd')]),
O([('a', 'b'), ('c', 'd')])
)
self.assertEqual(
f(['a', 'b']),
O([('a', 'a'), ('b', 'b')])
)
with self.assertRaises(AssertionError) as ctx:
f(None)
self.assertEqual(str(ctx.exception), "'fields' must be an iterable (e.g., a list, tuple, or mapping).")
with self.assertRaises(AssertionError) as ctx:
f([('a', 'b', 'c')])
self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.")
with self.assertRaises(AssertionError) as ctx:
f([0, 1, 2])
self.assertEqual(str(ctx.exception), "'fields' must contain strings or (field name, param name) pairs.")

View File

@ -4,13 +4,12 @@ import mock
import unittest
import django
from django.core.exceptions import ValidationError
from django.db import models
from django.test import TestCase
from django_filters.filterset import FilterSet
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
from django_filters.filterset import STRICTNESS
from django_filters.filters import Filter
from django_filters.filters import BooleanFilter
from django_filters.filters import CharFilter
from django_filters.filters import NumberFilter
@ -20,6 +19,8 @@ from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import UUIDFilter
from django_filters.filters import BaseInFilter
from django_filters.filters import BaseRangeFilter
from django_filters.filters import DateRangeFilter
from django_filters.filters import FilterMethod
from django_filters.widgets import BooleanWidget
@ -37,7 +38,6 @@ from .models import BankAccount
from .models import Node
from .models import DirectedNode
from .models import Worker
from .models import HiredWorker
from .models import Business
from .models import UUIDTestModel
@ -227,14 +227,15 @@ class FilterSetFilterForLookupTests(TestCase):
def test_isnull_with_filter_overrides(self):
class OFilterSet(FilterSet):
filter_overrides = {
models.BooleanField: {
'filter_class': BooleanFilter,
'extra': lambda f: {
'widget': BooleanWidget,
class Meta:
filter_overrides = {
models.BooleanField: {
'filter_class': BooleanFilter,
'extra': lambda f: {
'widget': BooleanWidget,
},
},
},
}
}
f = Article._meta.get_field('author')
result, params = OFilterSet.filter_for_lookup(f, 'isnull')
@ -392,9 +393,10 @@ class FilterSetClassCreationTests(TestCase):
class Meta:
model = Book
fields = ('username', 'price', 'other')
fields = ('username', 'price', 'other', 'another')
self.assertEqual(excinfo.exception.args, (
"Meta.fields contains a field that isn't defined on this FilterSet: other",))
"Meta.fields contains a field that isn't defined "
"on this FilterSet: other",))
def test_meta_fields_dictionary_containing_unknown(self):
with self.assertRaises(TypeError):
@ -405,7 +407,7 @@ class FilterSetClassCreationTests(TestCase):
fields = {'id': ['exact'],
'title': ['exact'],
'other': ['exact'],
}
}
def test_meta_exlude_with_declared_and_declared_wins(self):
class F(FilterSet):
@ -483,13 +485,14 @@ class FilterSetClassCreationTests(TestCase):
def test_custom_field_gets_filter_from_override(self):
class F(FilterSet):
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter}}
class Meta:
model = NetworkSetting
fields = '__all__'
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter}
}
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
def test_filterset_for_proxy_model(self):
@ -560,190 +563,6 @@ class FilterSetInstantiationTests(TestCase):
self.assertEqual(f.queryset, m)
class FilterSetOrderingTests(TestCase):
def setUp(self):
self.alex = User.objects.create(username='alex', status=1)
self.jacob = User.objects.create(username='jacob', status=2)
self.aaron = User.objects.create(username='aaron', status=2)
self.carl = User.objects.create(username='carl', status=0)
# user_ids = list(User.objects.all().values_list('pk', flat=True))
self.qs = User.objects.all().order_by('id')
def test_ordering_when_unbound(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F(queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
def test_ordering(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['username', 'status']
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
f = F({'o': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
def test_ordering_on_unknown_value(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, [], lambda o: o.username)
def test_ordering_on_unknown_value_results_in_default_ordering_without_strict(self):
class F(FilterSet):
strict = STRICTNESS.IGNORE
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
self.assertFalse(F.strict)
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
def test_ordering_on_unknown_value_results_in_default_ordering_with_strict_raise(self):
class F(FilterSet):
strict = STRICTNESS.RAISE_VALIDATION_ERROR
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F({'o': 'username'}, queryset=self.qs)
with self.assertRaises(ValidationError) as excinfo:
f.qs.all()
self.assertEqual(excinfo.exception.message_dict,
{'o': ['Select a valid choice. username is not one '
'of the available choices.']})
# No default order_by should get applied.
f = F({}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
def test_ordering_on_different_field(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
f = F({'o': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
def test_ordering_uses_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F({'o': 'account'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
def test_reverted_ordering_uses_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F({'o': '-account'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
def test_ordering_with_overridden_field_name(self):
"""
Set the `order_by_field` on the queryset and ensure that the
field name is respected.
"""
class F(FilterSet):
order_by_field = 'order'
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F({'order': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'jacob', 'aaron'], lambda o: o.username)
def test_ordering_descending_set(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['username', '-username']
f = F({'o': '-username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
def test_ordering_descending_unset(self):
""" Test ordering descending works when order_by=True. """
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F({'o': '-username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['jacob', 'carl', 'alex', 'aaron'], lambda o: o.username)
def test_custom_ordering(self):
class F(FilterSet):
debug = True
class Meta:
model = User
fields = ['username', 'status']
order_by = ['username', 'status']
def get_order_by(self, order_choice):
if order_choice == 'status':
return ['status', 'username']
return super(F, self).get_order_by(order_choice)
f = F({'o': 'username'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
f = F({'o': 'status'}, queryset=self.qs)
self.assertQuerysetEqual(
f.qs, ['carl', 'alex', 'aaron', 'jacob'], lambda o: o.username)
class FilterSetTogetherTests(TestCase):
def setUp(self):
@ -785,6 +604,106 @@ class FilterSetTogetherTests(TestCase):
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
# test filter.method here, as it depends on its parent FilterSet
class FilterMethodTests(TestCase):
def test_none(self):
# use a mock to bypass bound/unbound method equality
class TestFilter(Filter):
filter = mock.Mock()
f = TestFilter(method=None)
self.assertIsNone(f.method)
# passing method=None should not modify filter function
self.assertIs(f.filter, TestFilter.filter)
def test_method_name(self):
class F(FilterSet):
f = Filter(method='filter_f')
def filter_f(self, qs, name, value):
pass
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, 'filter_f')
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
def test_method_callable(self):
def filter_f(qs, name, value):
pass
class F(FilterSet):
f = Filter(method=filter_f)
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, filter_f)
self.assertEqual(f.filters['f'].filter.method, filter_f)
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
def test_method_with_overridden_filter(self):
# Some filter classes override the base filter() method. We need
# to ensure that passing a method argument still works correctly
class F(FilterSet):
f = DateRangeFilter(method='filter_f')
def filter_f(self, qs, name, value):
pass
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, 'filter_f')
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
def test_parent_unresolvable(self):
f = Filter(method='filter_f')
with self.assertRaises(AssertionError) as w:
f.filter(User.objects.all(), 0)
self.assertIn("'None'", str(w.exception))
self.assertIn('parent', str(w.exception))
self.assertIn('filter_f', str(w.exception))
def test_method_unresolvable(self):
class F(FilterSet):
f = Filter(method='filter_f')
f = F({}, queryset=User.objects.all())
with self.assertRaises(AssertionError) as w:
f.filters['f'].filter(User.objects.all(), 0)
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
self.assertIn('.filter_f()', str(w.exception))
def test_method_uncallable(self):
class F(FilterSet):
f = Filter(method='filter_f')
filter_f = 4
f = F({}, queryset=User.objects.all())
with self.assertRaises(AssertionError) as w:
f.filters['f'].filter(User.objects.all(), 0)
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
self.assertIn('.filter_f()', str(w.exception))
def test_method_set_unset(self):
# use a mock to bypass bound/unbound method equality
class TestFilter(Filter):
filter = mock.Mock()
f = TestFilter(method='filter_f')
self.assertEqual(f.method, 'filter_f')
self.assertIsInstance(f.filter, FilterMethod)
# setting None should revert to Filter.filter
f.method = None
self.assertIsNone(f.method)
self.assertIs(f.filter, TestFilter.filter)
@unittest.skip('TODO: remove when relevant deprecations have been completed')
class MiscFilterSetTests(TestCase):

View File

@ -164,116 +164,6 @@ class FilterSetFormTests(TestCase):
self.assertTrue(f.is_bound)
self.assertEqual(f.data, {'title': 'Some book'})
def test_ordering(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F().form
self.assertEqual(len(f.fields), 3)
self.assertIn('o', f.fields)
self.assertEqual(f.fields['o'].choices, [('status', 'Status')])
def test_ordering_uses_all_fields(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices,
[('username', 'Username'), ('-username', 'Username (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
def test_ordering_uses_filter_label(self):
class F(FilterSet):
username = CharFilter(label='Account')
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices,
[('username', 'Account'), ('-username', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
def test_ordering_uses_explicit_filter_name(self):
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
f = F().form
self.assertEqual(f.fields['o'].choices,
[('account', 'Account'), ('-account', 'Account (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
def test_ordering_with_overridden_field_name(self):
"""
Set the `order_by_field` on the queryset and ensure that the
field name is respected.
"""
class F(FilterSet):
order_by_field = 'order'
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status']
f = F().form
self.assertNotIn('o', f.fields)
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices, [('status', 'Status')])
def test_ordering_with_overridden_field_name_and_descending(self):
"""
Set the `order_by_field` on the queryset and ensure that the
field name is respected.
"""
class F(FilterSet):
order_by_field = 'order'
class Meta:
model = User
fields = ['username', 'status']
order_by = ['status', '-status']
f = F().form
self.assertNotIn('o', f.fields)
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices, [('status', 'Status'), ('-status', 'Status (descending)')])
def test_ordering_with_overridden_field_name_and_using_all_fields(self):
class F(FilterSet):
order_by_field = 'order'
class Meta:
model = User
fields = ['username', 'status']
order_by = True
f = F().form
self.assertIn('order', f.fields)
self.assertEqual(f.fields['order'].choices,
[('username', 'Username'), ('-username', 'Username (descending)'), ('status', 'Status'), ('-status', 'Status (descending)')])
def test_ordering_with_custom_display_names(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
order_by = [('status', 'Current status')]
f = F().form
self.assertEqual(
f.fields['o'].choices, [('status', 'Current status')])
def test_limit_choices_to(self):
User.objects.create(username='inactive', is_active=False, status=REGULAR)
User.objects.create(username='active', is_active=True, status=REGULAR)
@ -284,9 +174,9 @@ class FilterSetFormTests(TestCase):
model = ManagerGroup
fields = ['users', 'manager']
f = F().form
self.assertEquals(
self.assertEqual(
list(f.fields['users'].choices), [(2, 'active')]
)
self.assertEquals(
self.assertEqual(
list(f.fields['manager'].choices), [('', '---------'), (3, 'manager')]
)

View File

@ -78,15 +78,13 @@ class LinkWidgetTests(TestCase):
def test_widget_with_option_groups(self):
choices = (
('Audio', (
('vinyl', 'Vinyl'),
('cd', 'CD'),
)
),
('vinyl', 'Vinyl'),
('cd', 'CD'),
)),
('Video', (
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)
),
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)),
('unknown', 'Unknown'),
)

12
tox.ini
View File

@ -1,8 +1,8 @@
[tox]
envlist =
{py27,py33,py34,py35}-django18,
{py27,py34,py35}-django{19,110},
{py27,py34,py35}-django-latest
{py27,py33,py34,py35}-django18-restframework{33,34},
{py27,py34,py35}-django{19,110}-restframework34,
{py27,py34,py35}-django-latest-restframework34
[testenv]
@ -13,7 +13,7 @@ basepython =
py35: python3.5
commands = ./runtests.py
commands = ./runtests.py {posargs}
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
@ -21,4 +21,6 @@ deps =
django19: django>=1.9.0,<1.10.0
django110: django>=1.10.0,<1.11.0
django-latest: https://github.com/django/django/archive/master.tar.gz
-rrequirements/test.txt
restframework33: djangorestframework>=3.3,<3.4
restframework34: djangorestframework>=3.4,<3.5
-rrequirements/test-ci.txt