commit
a02db39fcb
|
@ -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]+))?
|
||||
|
|
27
.travis.yml
27
.travis.yml
|
@ -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
|
||||
|
|
34
CHANGES.rst
34
CHANGES.rst
|
@ -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)
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
------------
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
from .backends import DjangoFilterBackend
|
||||
from .filterset import FilterSet
|
||||
from .filters import *
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
|
@ -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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -14,6 +14,7 @@ Contents:
|
|||
|
||||
install
|
||||
usage
|
||||
rest_framework
|
||||
ref/filterset
|
||||
ref/filters
|
||||
ref/fields
|
||||
|
|
|
@ -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``.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
coverage
|
||||
mock
|
||||
pytz
|
|
@ -1,5 +1,3 @@
|
|||
-r maintainer.txt
|
||||
coverage==3.7.1
|
||||
django-discover-runner
|
||||
mock
|
||||
sphinx-autobuild
|
||||
-r travis-ci.txt
|
||||
django
|
||||
djangorestframework
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
-r test.txt
|
4
setup.py
4
setup.py
|
@ -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/*',
|
||||
],
|
||||
|
|
|
@ -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']
|
|
@ -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'}
|
||||
]
|
||||
)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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']
|
||||
|
|
|
@ -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'))
|
|
@ -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))
|
||||
|
|
|
@ -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')])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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')]
|
||||
)
|
||||
|
|
|
@ -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
12
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue