commit
f5b2d636d1
|
@ -1,5 +1,5 @@
|
|||
[bumpversion]
|
||||
current_version = 0.15.3
|
||||
current_version = 1.0.0
|
||||
commit = False
|
||||
tag = False
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
||||
|
|
45
.travis.yml
45
.travis.yml
|
@ -1,45 +1,16 @@
|
|||
language: python
|
||||
|
||||
sudo: false
|
||||
|
||||
language: python
|
||||
python: '3.5'
|
||||
cache: pip
|
||||
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
|
||||
env:
|
||||
- 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 -Ur requirements/test-ci.txt
|
||||
- travis_retry pip install $DJANGO $REST_FRAMEWORK
|
||||
- pip install coverage tox
|
||||
|
||||
script:
|
||||
- $W coverage run --source django_filters runtests.py -v 2
|
||||
- coverage erase
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- coverage combine
|
||||
- coverage report
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
matrix:
|
||||
exclude:
|
||||
- python: "3.3"
|
||||
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" REST_FRAMEWORK="djangorestframework>=3.4,<3.5"
|
||||
- python: "3.3"
|
||||
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: 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
|
||||
|
|
19
CHANGES.rst
19
CHANGES.rst
|
@ -1,3 +1,20 @@
|
|||
Version 1.0 (2016-11-17)
|
||||
------------------------
|
||||
|
||||
This release removes all the deprecated code from 0.14 and 0.15 for 1.0 #480.
|
||||
|
||||
Please see the `Migration Notes`__ for details of how to migrate.
|
||||
Stick with 0.15.3 if you're not ready to update.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/blob/develop/docs/migration.txt
|
||||
|
||||
The release includes a number of small fixes and documentation updates.
|
||||
|
||||
See the `1.0 Milestone`__ for full details.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/milestone/8?closed=1
|
||||
|
||||
|
||||
Version 0.15.3 (2016-10-17)
|
||||
---------------------------
|
||||
|
||||
|
@ -60,6 +77,8 @@ __ https://github.com/carltongibson/django-filter/blob/develop/docs/migration.tx
|
|||
Version 0.14.0 (2016-08-14)
|
||||
---------------------------
|
||||
|
||||
* Confirmed support for Django 1.10.
|
||||
|
||||
* Add support for filtering on DurationField (new in Django 1.8).
|
||||
|
||||
* Fix UUIDFilter import issue
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.PHONY: deps, test
|
||||
|
||||
deps:
|
||||
pip install -r ./requirements/test.txt
|
||||
|
||||
test:
|
||||
./runtests.py
|
|
@ -12,9 +12,9 @@ Full documentation on `read the docs`_.
|
|||
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
|
||||
* **Python**: 2.7, 3.3, 3.4, 3.5
|
||||
* **Django**: 1.8, 1.9, 1.10
|
||||
* **DRF**: 3.4, 3.5
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
from .constants import STRICTNESS
|
||||
from .filterset import FilterSet
|
||||
from .filters import *
|
||||
|
||||
__version__ = '0.15.3'
|
||||
__version__ = '1.0.0'
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
|
|
|
@ -14,9 +14,11 @@ except ImportError:
|
|||
is_crispy = 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
|
||||
|
||||
|
||||
# coreapi only compatible with DRF 3.4+
|
||||
# coreapi is optional (Note that uritemplate is a dependency of coreapi)
|
||||
# Fixes #525 - cannot simply import from rest_framework.compat, due to
|
||||
# import issues w/ django-guardian.
|
||||
try:
|
||||
from rest_framework.compat import coreapi
|
||||
import coreapi
|
||||
except ImportError:
|
||||
coreapi = None
|
||||
|
||||
|
|
|
@ -3,12 +3,22 @@ from django.conf import settings as dj_settings
|
|||
from django.core.signals import setting_changed
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .constants import STRICTNESS
|
||||
from .utils import deprecate
|
||||
|
||||
|
||||
DEFAULTS = {
|
||||
'DISABLE_HELP_TEXT': False,
|
||||
'HELP_TEXT_FILTER': True,
|
||||
'HELP_TEXT_EXCLUDE': True,
|
||||
|
||||
# empty/null choices
|
||||
'EMPTY_CHOICE_LABEL': '---------',
|
||||
'NULL_CHOICE_LABEL': None,
|
||||
'NULL_CHOICE_VALUE': 'null',
|
||||
|
||||
'STRICTNESS': STRICTNESS.RETURN_NO_RESULTS,
|
||||
|
||||
'VERBOSE_LOOKUPS': {
|
||||
# transforms don't need to be verbose, since their expressions are chained
|
||||
'date': _('date'),
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
|
||||
ALL_FIELDS = '__all__'
|
||||
|
||||
|
||||
class STRICTNESS:
|
||||
class IGNORE:
|
||||
pass
|
||||
|
||||
class RETURN_NO_RESULTS:
|
||||
pass
|
||||
|
||||
class RAISE_VALIDATION_ERROR:
|
||||
pass
|
||||
|
||||
# Values of False & True chosen for backward compatability reasons.
|
||||
# Originally, these were the only options.
|
||||
_LEGACY = {
|
||||
False: IGNORE,
|
||||
True: RETURN_NO_RESULTS,
|
||||
"RAISE": RAISE_VALIDATION_ERROR,
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
|
@ -19,11 +18,15 @@ from .fields import (
|
|||
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||
)
|
||||
from .utils import deprecate, pretty_name
|
||||
from .utils import label_for_filter, pretty_name
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AllValuesFilter',
|
||||
'AllValuesMultipleFilter',
|
||||
'BaseCSVFilter',
|
||||
'BaseInFilter',
|
||||
'BaseRangeFilter',
|
||||
'BooleanFilter',
|
||||
'CharFilter',
|
||||
'ChoiceFilter',
|
||||
|
@ -35,7 +38,6 @@ __all__ = [
|
|||
'DurationFilter',
|
||||
'Filter',
|
||||
'IsoDateTimeFilter',
|
||||
'MethodFilter',
|
||||
'ModelChoiceFilter',
|
||||
'ModelMultipleChoiceFilter',
|
||||
'MultipleChoiceFilter',
|
||||
|
@ -46,6 +48,7 @@ __all__ = [
|
|||
'TimeFilter',
|
||||
'TimeRangeFilter',
|
||||
'TypedChoiceFilter',
|
||||
'TypedMultipleChoiceFilter',
|
||||
'UUIDFilter',
|
||||
]
|
||||
|
||||
|
@ -56,27 +59,16 @@ 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)
|
||||
|
||||
|
||||
class Filter(object):
|
||||
creation_counter = 0
|
||||
field_class = forms.Field
|
||||
|
||||
def __init__(self, name=None, label=None, widget=None, action=None, method=None,
|
||||
lookup_expr='exact', required=False, distinct=False, exclude=False, **kwargs):
|
||||
def __init__(self, name=None, label=None, widget=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:
|
||||
_lookup_type_warning()
|
||||
self.lookup_expr = kwargs.pop('lookup_type')
|
||||
|
||||
self.widget = widget
|
||||
self.required = required
|
||||
|
@ -115,29 +107,28 @@ class Filter(object):
|
|||
return locals()
|
||||
method = property(**method())
|
||||
|
||||
def lookup_type():
|
||||
def label():
|
||||
def fget(self):
|
||||
_lookup_type_warning()
|
||||
return self.lookup_expr
|
||||
if self._label is None and hasattr(self, 'parent'):
|
||||
model = self.parent._meta.model
|
||||
self._label = label_for_filter(
|
||||
model, self.name, self.lookup_expr, self.exclude
|
||||
)
|
||||
return self._label
|
||||
|
||||
def fset(self, value):
|
||||
_lookup_type_warning()
|
||||
self.lookup_expr = value
|
||||
self._label = value
|
||||
|
||||
return locals()
|
||||
lookup_type = property(**lookup_type())
|
||||
label = property(**label())
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
if not hasattr(self, '_field'):
|
||||
help_text = self.extra.pop('help_text', None)
|
||||
if help_text is None:
|
||||
if self.exclude and settings.HELP_TEXT_EXCLUDE:
|
||||
help_text = _('This is an exclusion filter')
|
||||
elif not self.exclude and settings.HELP_TEXT_FILTER:
|
||||
help_text = _('Filter')
|
||||
else:
|
||||
help_text = ''
|
||||
field_kwargs = self.extra.copy()
|
||||
|
||||
if settings.DISABLE_HELP_TEXT:
|
||||
field_kwargs.pop('help_text', None)
|
||||
|
||||
if (self.lookup_expr is None or
|
||||
isinstance(self.lookup_expr, (list, tuple))):
|
||||
|
@ -161,12 +152,12 @@ class Filter(object):
|
|||
lookup.append(choice)
|
||||
|
||||
self._field = LookupTypeField(self.field_class(
|
||||
required=self.required, widget=self.widget, **self.extra),
|
||||
lookup, required=self.required, label=self.label, help_text=help_text)
|
||||
required=self.required, widget=self.widget, **field_kwargs),
|
||||
lookup, required=self.required, label=self.label)
|
||||
else:
|
||||
self._field = self.field_class(required=self.required,
|
||||
label=self.label, widget=self.widget,
|
||||
help_text=help_text, **self.extra)
|
||||
**field_kwargs)
|
||||
return self._field
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
@ -194,6 +185,39 @@ class BooleanFilter(Filter):
|
|||
class ChoiceFilter(Filter):
|
||||
field_class = forms.ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
|
||||
null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
|
||||
null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
|
||||
|
||||
self.null_value = null_value
|
||||
|
||||
if 'choices' in kwargs:
|
||||
choices = kwargs.get('choices')
|
||||
|
||||
# coerce choices to list
|
||||
if callable(choices):
|
||||
choices = choices()
|
||||
choices = list(choices)
|
||||
|
||||
# create the empty/null choices that prepend the original choices
|
||||
prepend = []
|
||||
if empty_label is not None:
|
||||
prepend.append(('', empty_label))
|
||||
if null_label is not None:
|
||||
prepend.append((null_value, null_label))
|
||||
|
||||
kwargs['choices'] = prepend + choices
|
||||
|
||||
super(ChoiceFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value != self.null_value:
|
||||
return super(ChoiceFilter, self).filter(qs, value)
|
||||
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): None})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
class TypedChoiceFilter(Filter):
|
||||
field_class = forms.TypedChoiceField
|
||||
|
@ -223,8 +247,8 @@ class MultipleChoiceFilter(Filter):
|
|||
`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` as to-many relationships will generally
|
||||
require this.
|
||||
"""
|
||||
field_class = forms.MultipleChoiceField
|
||||
|
||||
|
@ -278,6 +302,10 @@ class MultipleChoiceFilter(Filter):
|
|||
return {self.name: v}
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
|
||||
field_class = forms.TypedMultipleChoiceField
|
||||
|
||||
|
||||
class DateFilter(Filter):
|
||||
field_class = forms.DateField
|
||||
|
||||
|
@ -307,11 +335,62 @@ class DurationFilter(Filter):
|
|||
field_class = forms.DurationField
|
||||
|
||||
|
||||
class ModelChoiceFilter(Filter):
|
||||
class QuerySetRequestMixin(object):
|
||||
"""
|
||||
Add callable functionality to filters that support the ``queryset``
|
||||
argument. If the ``queryset`` is callable, then it **must** accept the
|
||||
``request`` object as a single argument.
|
||||
|
||||
This is useful for filtering querysets by properties on the ``request``
|
||||
object, such as the user.
|
||||
|
||||
Example::
|
||||
|
||||
def departments(request):
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
The above example restricts the set of departments to those in the logged-in
|
||||
user's associated company.
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.get('queryset')
|
||||
super(QuerySetRequestMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_request(self):
|
||||
try:
|
||||
return self.parent.request
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = self.queryset
|
||||
|
||||
if callable(queryset):
|
||||
return queryset(request)
|
||||
return queryset
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
request = self.get_request()
|
||||
queryset = self.get_queryset(request)
|
||||
|
||||
if queryset is not None:
|
||||
self.extra['queryset'] = queryset
|
||||
|
||||
return super(QuerySetRequestMixin, self).field
|
||||
|
||||
|
||||
class ModelChoiceFilter(QuerySetRequestMixin, Filter):
|
||||
field_class = forms.ModelChoiceField
|
||||
|
||||
|
||||
class ModelMultipleChoiceFilter(MultipleChoiceFilter):
|
||||
class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
|
||||
field_class = forms.ModelMultipleChoiceField
|
||||
|
||||
|
||||
|
@ -388,6 +467,10 @@ class DateRangeFilter(ChoiceFilter):
|
|||
def __init__(self, *args, **kwargs):
|
||||
kwargs['choices'] = [
|
||||
(key, value[0]) for key, value in six.iteritems(self.options)]
|
||||
|
||||
# empty/null choices not relevant
|
||||
kwargs.setdefault('empty_label', None)
|
||||
kwargs.setdefault('null_label', None)
|
||||
super(DateRangeFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
@ -440,6 +523,7 @@ class BaseCSVFilter(Filter):
|
|||
base_field_class = BaseCSVField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('help_text', _('Multiple values may be separated by commas.'))
|
||||
super(BaseCSVFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
class ConcreteCSVField(self.base_field_class, self.field_class):
|
||||
|
@ -535,6 +619,7 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter):
|
|||
kwargs['choices'] = self.build_choices(fields, field_labels)
|
||||
|
||||
kwargs.setdefault('label', _('Ordering'))
|
||||
kwargs.setdefault('null_label', None)
|
||||
super(OrderingFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_ordering_value(self, param):
|
||||
|
@ -576,61 +661,18 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter):
|
|||
|
||||
def build_choices(self, fields, labels):
|
||||
ascending = [
|
||||
(param, labels.get(field, pretty_name(param)))
|
||||
(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
|
||||
('-%s' % param, labels.get('-%s' % param, self.descending_fmt % label))
|
||||
for param, label 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)
|
||||
|
||||
# If the action is a string store the action and set the action to our own filter method
|
||||
# so it can be backwards compatible and work as expected, the parent will still treat it as
|
||||
# a filter that has an action
|
||||
self.parent_action = ''
|
||||
text_types = (str, six.text_type)
|
||||
if type(action) in text_types:
|
||||
self.parent_action = str(action)
|
||||
kwargs.update({
|
||||
'action': self.filter
|
||||
})
|
||||
|
||||
# Call the parent
|
||||
super(MethodFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
"""
|
||||
This filter method will act as a proxy for the actual method we want to
|
||||
call.
|
||||
|
||||
It will try to find the method on the parent filterset,
|
||||
if not it attempts to search for the method `field_{{attribute_name}}`.
|
||||
Otherwise it defaults to just returning the queryset.
|
||||
"""
|
||||
parent = getattr(self, 'parent', None)
|
||||
parent_filter_method = getattr(parent, self.parent_action, None)
|
||||
if not parent_filter_method:
|
||||
func_str = 'filter_{0}'.format(self.name)
|
||||
parent_filter_method = getattr(parent, func_str, None)
|
||||
if parent_filter_method is not None:
|
||||
return parent_filter_method(qs, value)
|
||||
return qs
|
||||
|
||||
|
||||
class FilterMethod(object):
|
||||
"""
|
||||
This helper is used to override Filter.filter() when a 'method' argument
|
||||
|
|
|
@ -10,99 +10,30 @@ from django.db import models
|
|||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.utils import six
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .conf import settings
|
||||
from .compat import remote_field, remote_queryset
|
||||
from .constants import ALL_FIELDS, STRICTNESS
|
||||
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
|
||||
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter,
|
||||
DurationFilter, OrderingFilter)
|
||||
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field, pretty_name, deprecate
|
||||
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter, DurationFilter)
|
||||
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
|
||||
|
||||
|
||||
ORDER_BY_FIELD = 'o'
|
||||
|
||||
|
||||
class STRICTNESS(object):
|
||||
def get_filter_name(field_name, lookup_expr):
|
||||
"""
|
||||
Values of False & True chosen for backward compatability reasons.
|
||||
Originally, these were the only options.
|
||||
Combine a field name and lookup expression into a usable filter name.
|
||||
Exact lookups are the implicit default, so "exact" is stripped from the
|
||||
end of the filter name.
|
||||
"""
|
||||
IGNORE = False
|
||||
RETURN_NO_RESULTS = True
|
||||
RAISE_VALIDATION_ERROR = "RAISE"
|
||||
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
|
||||
|
||||
# This also works with transformed exact lookups, such as 'date__exact'
|
||||
_exact = LOOKUP_SEP + 'exact'
|
||||
if filter_name.endswith(_exact):
|
||||
filter_name = filter_name[:-len(_exact)]
|
||||
|
||||
def get_declared_filters(bases, attrs, with_base_filters=True):
|
||||
filters = []
|
||||
for filter_name, obj in list(attrs.items()):
|
||||
if isinstance(obj, Filter):
|
||||
obj = attrs.pop(filter_name)
|
||||
if getattr(obj, 'name', None) is None:
|
||||
obj.name = filter_name
|
||||
filters.append((filter_name, obj))
|
||||
filters.sort(key=lambda x: x[1].creation_counter)
|
||||
|
||||
if with_base_filters:
|
||||
for base in bases[::-1]:
|
||||
if hasattr(base, 'base_filters'):
|
||||
filters = list(base.base_filters.items()) + filters
|
||||
else:
|
||||
for base in bases[::-1]:
|
||||
if hasattr(base, 'declared_filters'):
|
||||
filters = list(base.declared_filters.items()) + filters
|
||||
|
||||
return OrderedDict(filters)
|
||||
|
||||
|
||||
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
||||
filter_for_reverse_field=None):
|
||||
field_dict = OrderedDict()
|
||||
|
||||
# Setting exclude with no fields implies all other fields.
|
||||
if exclude is not None and fields is None:
|
||||
fields = '__all__'
|
||||
|
||||
# All implies all db fields associated with a filter_class.
|
||||
if fields == '__all__':
|
||||
fields = get_all_model_fields(model)
|
||||
|
||||
# Loop through the list of fields.
|
||||
for f in fields:
|
||||
# Skip the field if excluded.
|
||||
if exclude is not None and f in exclude:
|
||||
continue
|
||||
field = get_model_field(model, f)
|
||||
# Do nothing if the field doesn't exist.
|
||||
if field is None:
|
||||
field_dict[f] = None
|
||||
continue
|
||||
if isinstance(field, ForeignObjectRel):
|
||||
filter_ = filter_for_reverse_field(field, f)
|
||||
if filter_:
|
||||
field_dict[f] = filter_
|
||||
# If fields is a dictionary, it must contain lists.
|
||||
elif isinstance(fields, dict):
|
||||
# Create a filter for each lookup type.
|
||||
for lookup_expr in fields[f]:
|
||||
filter_ = filter_for_field(field, f, lookup_expr)
|
||||
|
||||
if filter_:
|
||||
filter_name = LOOKUP_SEP.join([f, lookup_expr])
|
||||
|
||||
# Don't add "exact" to filter names
|
||||
_exact = LOOKUP_SEP + 'exact'
|
||||
if filter_name.endswith(_exact):
|
||||
filter_name = filter_name[:-len(_exact)]
|
||||
|
||||
field_dict[filter_name] = filter_
|
||||
# If fields is a list, it contains strings.
|
||||
else:
|
||||
filter_ = filter_for_field(field, f)
|
||||
if filter_:
|
||||
field_dict[f] = filter_
|
||||
return field_dict
|
||||
return filter_name
|
||||
|
||||
|
||||
def get_full_clean_override(together):
|
||||
|
@ -132,35 +63,13 @@ def get_full_clean_override(together):
|
|||
|
||||
class FilterSetOptions(object):
|
||||
def __init__(self, options=None):
|
||||
if getattr(options, 'model', None) is not None:
|
||||
if not hasattr(options, 'fields') and not hasattr(options, 'exclude'):
|
||||
deprecate(
|
||||
"Not setting Meta.fields with Meta.model is undocumented behavior "
|
||||
"and may result in unintentionally exposing filter fields. This has "
|
||||
"been deprecated in favor of setting Meta.fields = '__all__' or by "
|
||||
"setting the Meta.exclude attribute.", 1)
|
||||
|
||||
elif getattr(options, 'fields', -1) is None:
|
||||
deprecate(
|
||||
"Setting 'Meta.fields = None' is undocumented behavior and has been "
|
||||
"deprecated in favor of Meta.fields = '__all__'.", 1)
|
||||
|
||||
self.model = getattr(options, 'model', None)
|
||||
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.strict = getattr(options, 'strict', None)
|
||||
|
||||
self.form = getattr(options, 'form', forms.Form)
|
||||
|
||||
|
@ -169,64 +78,36 @@ class FilterSetOptions(object):
|
|||
|
||||
class FilterSetMetaclass(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
try:
|
||||
parents = [b for b in bases if issubclass(b, FilterSet)]
|
||||
except NameError:
|
||||
# We are defining FilterSet itself here
|
||||
parents = None
|
||||
declared_filters = get_declared_filters(bases, attrs, False)
|
||||
new_class = super(
|
||||
FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
|
||||
|
||||
if not parents:
|
||||
return new_class
|
||||
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
|
||||
new_class.base_filters = new_class.get_filters()
|
||||
|
||||
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:
|
||||
filters = new_class.filters_for_model(opts.model, opts)
|
||||
filters.update(declared_filters)
|
||||
else:
|
||||
filters = declared_filters
|
||||
|
||||
not_defined = next((k for k, v in filters.items() if v is None), False)
|
||||
if not_defined:
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def get_declared_filters(cls, bases, attrs):
|
||||
filters = [
|
||||
(filter_name, attrs.pop(filter_name))
|
||||
for filter_name, obj in list(attrs.items())
|
||||
if isinstance(obj, Filter)
|
||||
]
|
||||
|
||||
# Default the `filter.name` to the attribute name on the filterset
|
||||
for filter_name, f in filters:
|
||||
if getattr(f, 'name', None) is None:
|
||||
f.name = filter_name
|
||||
|
||||
filters.sort(key=lambda x: x[1].creation_counter)
|
||||
|
||||
# merge declared filters from base classes
|
||||
for base in reversed(bases):
|
||||
if hasattr(base, 'declared_filters'):
|
||||
filters = list(base.declared_filters.items()) + filters
|
||||
|
||||
return OrderedDict(filters)
|
||||
|
||||
|
||||
FILTER_FOR_DBFIELD_DEFAULTS = {
|
||||
models.AutoField: {'filter_class': NumberFilter},
|
||||
|
@ -277,7 +158,7 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
|
|||
class BaseFilterSet(object):
|
||||
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
|
||||
|
||||
def __init__(self, data=None, queryset=None, prefix=None, strict=None):
|
||||
def __init__(self, data=None, queryset=None, prefix=None, strict=None, request=None):
|
||||
self.is_bound = data is not None
|
||||
self.data = data or {}
|
||||
if queryset is None:
|
||||
|
@ -286,43 +167,35 @@ class BaseFilterSet(object):
|
|||
self.form_prefix = prefix
|
||||
|
||||
# What to do on on validation errors
|
||||
self.strict = self._meta.strict if strict is None else strict
|
||||
# Fallback to meta, then settings strictness
|
||||
if strict is None:
|
||||
strict = self._meta.strict
|
||||
if strict is None:
|
||||
strict = settings.STRICTNESS
|
||||
|
||||
# transform legacy values
|
||||
self.strict = STRICTNESS._LEGACY.get(strict, strict)
|
||||
|
||||
self.request = request
|
||||
|
||||
self.filters = copy.deepcopy(self.base_filters)
|
||||
# propagate the model being used through the filters
|
||||
|
||||
for filter_ in self.filters.values():
|
||||
# propagate the model and filterset to the filters
|
||||
filter_.model = self._meta.model
|
||||
|
||||
# Apply the parent to the filters, this will allow the filters to access the filterset
|
||||
for filter_key, filter_ in six.iteritems(self.filters):
|
||||
filter_.parent = self
|
||||
|
||||
def __iter__(self):
|
||||
deprecate('QuerySet methods are no longer proxied.')
|
||||
for obj in self.qs:
|
||||
yield obj
|
||||
|
||||
def __len__(self):
|
||||
deprecate('QuerySet methods are no longer proxied.')
|
||||
return self.qs.count()
|
||||
|
||||
def __getitem__(self, key):
|
||||
deprecate('QuerySet methods are no longer proxied.')
|
||||
return self.qs[key]
|
||||
|
||||
def count(self):
|
||||
deprecate('QuerySet methods are no longer proxied.')
|
||||
return self.qs.count()
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
if not hasattr(self, '_qs'):
|
||||
valid = self.is_bound and self.form.is_valid()
|
||||
if not self.is_bound:
|
||||
self._qs = self.queryset.all()
|
||||
return self._qs
|
||||
|
||||
if self.is_bound and not valid:
|
||||
if not self.form.is_valid():
|
||||
if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
|
||||
raise forms.ValidationError(self.form.errors)
|
||||
elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:
|
||||
elif self.strict == STRICTNESS.RETURN_NO_RESULTS:
|
||||
self._qs = self.queryset.none()
|
||||
return self._qs
|
||||
# else STRICTNESS.IGNORE... ignoring
|
||||
|
@ -330,20 +203,7 @@ class BaseFilterSet(object):
|
|||
# start with all the results and filter from there
|
||||
qs = self.queryset.all()
|
||||
for name, filter_ in six.iteritems(self.filters):
|
||||
value = None
|
||||
if valid:
|
||||
value = self.form.cleaned_data[name]
|
||||
else:
|
||||
raw_value = self.form[name].value()
|
||||
try:
|
||||
value = self.form.fields[name].clean(raw_value)
|
||||
except forms.ValidationError:
|
||||
if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
|
||||
raise
|
||||
elif bool(self.strict) == STRICTNESS.RETURN_NO_RESULTS:
|
||||
self._qs = self.queryset.none()
|
||||
return self._qs
|
||||
# else STRICTNESS.IGNORE... ignoring
|
||||
value = self.form.cleaned_data.get(name)
|
||||
|
||||
if value is not None: # valid & clean data
|
||||
qs = filter_.filter(qs, value)
|
||||
|
@ -370,62 +230,88 @@ class BaseFilterSet(object):
|
|||
return self._form
|
||||
|
||||
@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 get_fields(cls):
|
||||
"""
|
||||
Resolve the 'fields' argument that should be used for generating filters on the
|
||||
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
|
||||
"""
|
||||
model = cls._meta.model
|
||||
fields = cls._meta.fields
|
||||
exclude = cls._meta.exclude
|
||||
|
||||
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)
|
||||
assert not (fields is None and exclude is None), \
|
||||
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
|
||||
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit" \
|
||||
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
|
||||
|
||||
return pretty_name(text)
|
||||
# Setting exclude with no fields implies all other fields.
|
||||
if exclude is not None and fields is None:
|
||||
fields = ALL_FIELDS
|
||||
|
||||
if isinstance(opts.order_by, (list, tuple)):
|
||||
# Resolve ALL_FIELDS into all fields for the filterset's model.
|
||||
if fields == ALL_FIELDS:
|
||||
fields = get_all_model_fields(model)
|
||||
|
||||
# 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)
|
||||
# Remove excluded fields
|
||||
exclude = exclude or []
|
||||
if not isinstance(fields, dict):
|
||||
fields = [(f, ['exact']) for f in fields if f not in exclude]
|
||||
else:
|
||||
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
|
||||
|
||||
# e.g. ('field1', 'field2', ...)
|
||||
else:
|
||||
# (filter name, filter instance)
|
||||
order_by = [(f, filters.get(f.lstrip('-'))) for f in opts.order_by]
|
||||
|
||||
# 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)
|
||||
|
||||
# opts.order_by = True
|
||||
order_by = filters.items()
|
||||
|
||||
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)
|
||||
return OrderedDict(fields)
|
||||
|
||||
@classmethod
|
||||
def filters_for_model(cls, model, opts):
|
||||
# TODO: remove with deprecations - this emulates the old behavior
|
||||
fields = opts.fields
|
||||
if fields is None:
|
||||
DEFAULTS = dict(FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
DEFAULTS.update(opts.filter_overrides)
|
||||
fields = get_all_model_fields(model, field_types=DEFAULTS.keys())
|
||||
def get_filters(cls):
|
||||
"""
|
||||
Get all filters for the filterset. This is the combination of declared and
|
||||
generated filters.
|
||||
"""
|
||||
|
||||
return filters_for_model(
|
||||
model, fields, opts.exclude,
|
||||
cls.filter_for_field,
|
||||
cls.filter_for_reverse_field
|
||||
)
|
||||
# No model specified - skip filter generation
|
||||
if not cls._meta.model:
|
||||
return cls.declared_filters.copy()
|
||||
|
||||
# Determine the filters that should be included on the filterset.
|
||||
filters = OrderedDict()
|
||||
fields = cls.get_fields()
|
||||
undefined = []
|
||||
|
||||
for field_name, lookups in fields.items():
|
||||
field = get_model_field(cls._meta.model, field_name)
|
||||
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
undefined.append(field_name)
|
||||
|
||||
# ForeignObjectRel does not support non-exact lookups
|
||||
if isinstance(field, ForeignObjectRel):
|
||||
filters[field_name] = cls.filter_for_reverse_field(field, field_name)
|
||||
continue
|
||||
|
||||
for lookup_expr in lookups:
|
||||
filter_name = get_filter_name(field_name, lookup_expr)
|
||||
|
||||
# If the filter is explicitly declared on the class, skip generation
|
||||
if filter_name in cls.declared_filters:
|
||||
filters[filter_name] = cls.declared_filters[filter_name]
|
||||
continue
|
||||
|
||||
if field is not None:
|
||||
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
|
||||
# filter out declared filters
|
||||
undefined = [f for f in undefined if f not in cls.declared_filters]
|
||||
if undefined:
|
||||
raise TypeError(
|
||||
"'Meta.fields' contains fields that are not defined on this FilterSet: "
|
||||
"%s" % ', '.join(undefined)
|
||||
)
|
||||
|
||||
# Add in declared filters. This is necessary since we don't enforce adding
|
||||
# declared filters to the 'Meta.fields' option
|
||||
filters.update(cls.declared_filters)
|
||||
return filters
|
||||
|
||||
@classmethod
|
||||
def filter_for_field(cls, f, name, lookup_expr='exact'):
|
||||
|
@ -433,8 +319,7 @@ class BaseFilterSet(object):
|
|||
|
||||
default = {
|
||||
'name': name,
|
||||
'label': capfirst(f.verbose_name),
|
||||
'lookup_expr': lookup_expr
|
||||
'lookup_expr': lookup_expr,
|
||||
}
|
||||
|
||||
filter_class, params = cls.filter_for_lookup(f, lookup_type)
|
||||
|
@ -454,7 +339,6 @@ class BaseFilterSet(object):
|
|||
queryset = f.field.model._default_manager.all()
|
||||
default = {
|
||||
'name': name,
|
||||
'label': capfirst(rel.related_name),
|
||||
'queryset': queryset,
|
||||
}
|
||||
if rel.multiple:
|
||||
|
@ -538,7 +422,7 @@ class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
|
|||
|
||||
|
||||
def filterset_factory(model):
|
||||
meta = type(str('Meta'), (object,), {'model': model, 'fields': '__all__'})
|
||||
meta = type(str('Meta'), (object,), {'model': model, 'fields': ALL_FIELDS})
|
||||
filterset = type(str('%sFilterSet' % model._meta.object_name),
|
||||
(FilterSet,), {'Meta': meta})
|
||||
return filterset
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,48 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-filter\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-08-10 05:34-0500\n"
|
||||
"PO-Revision-Date: 2016-09-29 11:47+0300\n"
|
||||
"Last-Translator: Mikhail Mitrofanov <mm@elec.ru>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 1.8.9\n"
|
||||
|
||||
#: filters.py:153
|
||||
msgid "Any date"
|
||||
msgstr "Любая дата"
|
||||
|
||||
#: filters.py:154
|
||||
msgid "Today"
|
||||
msgstr "Сегодня"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Past 7 days"
|
||||
msgstr "Прошедшие 7 дней"
|
||||
|
||||
#: filters.py:163
|
||||
msgid "This month"
|
||||
msgstr "За этот месяц"
|
||||
|
||||
#: filters.py:167
|
||||
msgid "This year"
|
||||
msgstr "В этом году"
|
||||
|
||||
#: filterset.py:332 filterset.py:341
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (по убыванию)"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Все"
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.template import Template, TemplateDoesNotExist, loader
|
||||
from django.utils import six
|
||||
from rest_framework.compat import template_render
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
|
||||
|
@ -71,7 +72,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
filter_class = self.get_filter_class(view, queryset)
|
||||
|
||||
if filter_class:
|
||||
return filter_class(request.query_params, queryset=queryset).qs
|
||||
return filter_class(request.query_params, queryset=queryset, request=request).qs
|
||||
|
||||
return queryset
|
||||
|
||||
|
@ -79,7 +80,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
filter_class = self.get_filter_class(view, queryset)
|
||||
if not filter_class:
|
||||
return None
|
||||
filter_instance = filter_class(request.query_params, queryset=queryset)
|
||||
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
|
||||
|
||||
try:
|
||||
template = loader.get_template(self.template)
|
||||
|
@ -98,6 +99,7 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
filter_class = self.get_filter_class(view, view.get_queryset())
|
||||
|
||||
return [] if not filter_class else [
|
||||
compat.coreapi.Field(name=field_name, required=False, location='query')
|
||||
for field_name in filter_class().filters.keys()
|
||||
compat.coreapi.Field(
|
||||
name=field_name, required=False, location='query', description=six.text_type(field.field.help_text))
|
||||
for field_name, field in filter_class().filters.items()
|
||||
]
|
||||
|
|
|
@ -6,8 +6,10 @@ from django.db import models
|
|||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.expressions import Expression
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.db.models.fields.related import RelatedField, ForeignObjectRel
|
||||
from django.utils import six, timezone
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
try:
|
||||
from django.forms.utils import pretty_name
|
||||
|
@ -44,18 +46,9 @@ def try_dbfield(fn, field_class):
|
|||
return data
|
||||
|
||||
|
||||
# TODO: remove field_types arg with deprecations
|
||||
def get_all_model_fields(model, field_types=None):
|
||||
def get_all_model_fields(model):
|
||||
opts = model._meta
|
||||
|
||||
if field_types is not None:
|
||||
return [
|
||||
f.name for f in sorted(opts.fields + opts.many_to_many)
|
||||
if not isinstance(f, models.AutoField) and
|
||||
not (getattr(remote_field(f), 'parent_link', False)) and
|
||||
f.__class__ in field_types
|
||||
]
|
||||
|
||||
return [
|
||||
f.name for f in sorted(opts.fields + opts.many_to_many)
|
||||
if not isinstance(f, models.AutoField) and
|
||||
|
@ -72,25 +65,41 @@ def get_model_field(model, field_name):
|
|||
|
||||
f = get_model_field(Book, 'author__first_name')
|
||||
|
||||
"""
|
||||
fields = get_field_parts(model, field_name)
|
||||
return fields[-1] if fields else None
|
||||
|
||||
|
||||
def get_field_parts(model, field_name):
|
||||
"""
|
||||
Get the field parts that represent the traversable relationships from the
|
||||
base ``model`` to the final field, described by ``field_name``.
|
||||
|
||||
ex::
|
||||
|
||||
>>> parts = get_field_parts(Book, 'author__first_name')
|
||||
>>> [p.verbose_name for p in parts]
|
||||
['author', 'first name']
|
||||
|
||||
"""
|
||||
parts = field_name.split(LOOKUP_SEP)
|
||||
opts = model._meta
|
||||
fields = []
|
||||
|
||||
# walk relationships
|
||||
for name in parts[:-1]:
|
||||
for name in parts:
|
||||
try:
|
||||
rel = opts.get_field(name)
|
||||
field = opts.get_field(name)
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
if isinstance(rel, ForeignObjectRel):
|
||||
opts = rel.related_model._meta
|
||||
else:
|
||||
opts = remote_model(rel)._meta
|
||||
|
||||
try:
|
||||
return opts.get_field(parts[-1])
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
fields.append(field)
|
||||
if isinstance(field, RelatedField):
|
||||
opts = remote_model(field)._meta
|
||||
elif isinstance(field, ForeignObjectRel):
|
||||
opts = field.related_model._meta
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def resolve_field(model_field, lookup_expr):
|
||||
|
@ -139,3 +148,82 @@ def handle_timezone(value):
|
|||
elif not settings.USE_TZ and timezone.is_aware(value):
|
||||
return timezone.make_naive(value, timezone.UTC())
|
||||
return value
|
||||
|
||||
|
||||
def verbose_field_name(model, field_name):
|
||||
"""
|
||||
Get the verbose name for a given ``field_name``. The ``field_name``
|
||||
will be traversed across relationships. Returns '[invalid name]' for
|
||||
any field name that cannot be traversed.
|
||||
|
||||
ex::
|
||||
|
||||
>>> verbose_field_name(Article, 'author__name')
|
||||
'author name'
|
||||
|
||||
"""
|
||||
if field_name is None:
|
||||
return '[invalid name]'
|
||||
|
||||
parts = get_field_parts(model, field_name)
|
||||
if not parts:
|
||||
return '[invalid name]'
|
||||
|
||||
names = []
|
||||
for part in parts:
|
||||
if isinstance(part, ForeignObjectRel):
|
||||
names.append(force_text(part.related_name))
|
||||
else:
|
||||
names.append(force_text(part.verbose_name))
|
||||
|
||||
return ' '.join(names)
|
||||
|
||||
|
||||
def verbose_lookup_expr(lookup_expr):
|
||||
"""
|
||||
Get a verbose, more humanized expression for a given ``lookup_expr``.
|
||||
Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
|
||||
dictionary. Missing keys will simply default to itself.
|
||||
|
||||
ex::
|
||||
|
||||
>>> verbose_lookup_expr('year__lt')
|
||||
'year is less than'
|
||||
|
||||
# with `FILTERS_VERBOSE_LOOKUPS = {}`
|
||||
>>> verbose_lookup_expr('year__lt')
|
||||
'year lt'
|
||||
|
||||
"""
|
||||
from .conf import settings as app_settings
|
||||
|
||||
VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
|
||||
lookups = [
|
||||
force_text(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
|
||||
for lookup in lookup_expr.split(LOOKUP_SEP)
|
||||
]
|
||||
|
||||
return ' '.join(lookups)
|
||||
|
||||
|
||||
def label_for_filter(model, field_name, lookup_expr, exclude=False):
|
||||
"""
|
||||
Create a generic label suitable for a filter.
|
||||
|
||||
ex::
|
||||
|
||||
>>> label_for_filter(Article, 'author__name', 'in')
|
||||
'auther name is in'
|
||||
|
||||
"""
|
||||
name = verbose_field_name(model, field_name)
|
||||
verbose_expression = [_('exclude'), name] if exclude else [name]
|
||||
|
||||
# iterable lookups indicate a LookupTypeField, which should not be verbose
|
||||
if isinstance(lookup_expr, six.string_types):
|
||||
verbose_expression += [verbose_lookup_expr(lookup_expr)]
|
||||
|
||||
verbose_expression = [force_text(part) for part in verbose_expression if part]
|
||||
verbose_expression = pretty_name(' '.join(verbose_expression))
|
||||
|
||||
return verbose_expression
|
||||
|
|
|
@ -36,7 +36,10 @@ class FilterMixin(object):
|
|||
"""
|
||||
Returns the keyword arguments for instanciating the filterset.
|
||||
"""
|
||||
kwargs = {'data': self.request.GET or None}
|
||||
kwargs = {
|
||||
'data': self.request.GET or None,
|
||||
'request': self.request,
|
||||
}
|
||||
try:
|
||||
kwargs.update({
|
||||
'queryset': self.get_queryset(),
|
||||
|
|
|
@ -11,6 +11,7 @@ except:
|
|||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms.widgets import flatatt
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.six import string_types
|
||||
|
@ -171,3 +172,38 @@ class BaseCSVWidget(forms.Widget):
|
|||
|
||||
class CSVWidget(BaseCSVWidget, forms.TextInput):
|
||||
pass
|
||||
|
||||
|
||||
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
|
||||
"""
|
||||
Enables request query array notation that might be consumed by MultipleChoiceFilter
|
||||
|
||||
1. Values can be provided as csv string: ?foo=bar,baz
|
||||
2. Values can be provided as query array: ?foo[]=bar&foo[]=baz
|
||||
3. Values can be provided as query array: ?foo=bar&foo=baz
|
||||
|
||||
Note: Duplicate and empty values are skipped from results
|
||||
"""
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if not isinstance(data, MultiValueDict):
|
||||
data = MultiValueDict(data)
|
||||
|
||||
values_list = data.getlist(name, data.getlist('%s[]' % name)) or []
|
||||
|
||||
if isinstance(values_list, string_types):
|
||||
values_list = [values_list]
|
||||
|
||||
# apparently its an array, so no need to process it's values as csv
|
||||
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
|
||||
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
|
||||
if len(values_list) > 1:
|
||||
ret = [x for x in values_list if x]
|
||||
elif len(values_list) == 1:
|
||||
# treat first element as csv string
|
||||
# ?foo=1,2 -> data.getlist(foo) -> foo = ['1,2']
|
||||
ret = [x.strip() for x in values_list[0].rstrip(',').split(',') if x]
|
||||
else:
|
||||
ret = []
|
||||
|
||||
return list(set(ret))
|
||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.15.3'
|
||||
version = '1.0.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.15.3'
|
||||
release = '1.0.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
======================
|
||||
Running the Test Suite
|
||||
======================
|
||||
|
||||
The easiest way to run the django-filter tests is to check out the source
|
||||
code and create a virtualenv where you can install the test dependencies.
|
||||
Django-filter uses a custom test runner to configure the environment, so a
|
||||
wrapper script is available to set up and run the test suite.
|
||||
|
||||
.. note::
|
||||
|
||||
The following assumes you have `virtualenv`__ and `git`__ installed.
|
||||
|
||||
__ https://virtualenv.pypa.io/en/stable/
|
||||
__ https://git-scm.com
|
||||
|
||||
Clone the repository
|
||||
--------------------
|
||||
|
||||
Get the source code using the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/carltongibson/django-filter.git
|
||||
|
||||
Switch to the django-filter directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd django-filter
|
||||
|
||||
Set up the virtualenv
|
||||
---------------------
|
||||
|
||||
Create a new virtualenv to run the test suite in:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ virtualenv venv
|
||||
|
||||
Then activate the virtualenv and install the test requirements:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ source venv/bin/activate
|
||||
$ pip install -r requirements/test.txt
|
||||
|
||||
Execute the test runner
|
||||
-----------------------
|
||||
|
||||
Run the tests with the runner script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python runtests.py
|
||||
|
||||
|
||||
Test 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, and then simply run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install tox
|
||||
$ tox
|
|
@ -0,0 +1,33 @@
|
|||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
Django-filter can be installed from PyPI with tools like ``pip``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install django-filter
|
||||
|
||||
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||
|
||||
.. note::
|
||||
|
||||
django-filter provides *some* localization for *some* languages. If you do
|
||||
not need these translations (or would rather provide your own), then it is
|
||||
unnecessary to add django-filter to the ``INSTALLED_APPS`` setting.
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Django-filter is tested against all supported versions of Python and `Django`__,
|
||||
as well as the latest versions of Django REST Framework (`DRF`__).
|
||||
|
||||
__ https://www.djangoproject.com/download/
|
||||
__ http://www.django-rest-framework.org/
|
||||
|
||||
|
||||
|
||||
* **Python**: 2.7, 3.3, 3.4, 3.5
|
||||
* **Django**: 1.8, 1.9, 1.10
|
||||
* **DRF**: 3.4, 3.5
|
|
@ -1,3 +1,4 @@
|
|||
================
|
||||
Migrating to 1.0
|
||||
================
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
Django Rest Framework
|
||||
=====================
|
||||
====================
|
||||
Integration with DRF
|
||||
====================
|
||||
|
||||
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.
|
||||
|
||||
|
@ -72,8 +73,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet
|
|||
filter_class = ProductFilter
|
||||
|
||||
|
||||
Specifying ``filter_fields``
|
||||
----------------------------
|
||||
Using the ``filter_fields`` shortcut
|
||||
------------------------------------
|
||||
|
||||
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>`.
|
||||
|
||||
|
@ -98,14 +99,74 @@ You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to y
|
|||
fields = ('category', 'in_stock')
|
||||
|
||||
|
||||
Schema Generation with Core API
|
||||
-------------------------------
|
||||
|
||||
The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()``. This is automatically enabled when Core API is installed. Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``.
|
||||
|
||||
For example, your get queryset method may look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_project(self):
|
||||
return models.Project.objects.get(pk=self.kwargs['project_id'])
|
||||
|
||||
def get_queryset(self):
|
||||
project = self.get_project()
|
||||
|
||||
return self.queryset \
|
||||
.filter(project=project) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
This could be rewritten like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_project(self):
|
||||
try:
|
||||
return models.Project.objects.get(pk=self.kwargs['project_id'])
|
||||
except models.Project.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_queryset(self):
|
||||
project = self.get_project()
|
||||
|
||||
if project is None:
|
||||
return self.queryset.none()
|
||||
|
||||
return self.queryset \
|
||||
.filter(project=project) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
Or more simply as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
# project_id may be None
|
||||
return self.queryset \
|
||||
.filter(project_id=self.kwargs.get('project_id')) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
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
|
||||
.. image:: ../assets/form.png
|
|
@ -0,0 +1,215 @@
|
|||
==================
|
||||
Tips and Solutions
|
||||
==================
|
||||
|
||||
Common problems for declared filters
|
||||
------------------------------------
|
||||
|
||||
Below are some of the common problem that occur when declaring filters. It is
|
||||
recommended that you read this as it provides a more complete understanding of
|
||||
how filters work.
|
||||
|
||||
|
||||
Filter ``name`` and ``lookup_expr`` not configured
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
|
||||
them. By default, if ``name`` is not specified, the filter's name on the
|
||||
filterset class will be used. Additionally, ``lookup_expr`` defaults to
|
||||
``exact``. The following is an example of a misconfigured price filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter()
|
||||
|
||||
The filter instance will have a field name of ``price__gt`` and an ``exact``
|
||||
lookup type. Under the hood, this will incorrectly be resolved as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Produce.objects.filter(price__gt__exact=value)
|
||||
|
||||
The above will most likely generate a ``FieldError``. The correct configuration
|
||||
would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
|
||||
|
||||
|
||||
Missing ``lookup_expr`` for text search filters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's quite common to forget to set the lookup expression for :code:`CharField`
|
||||
and :code:`TextField` and wonder why a search for "foo" does not return results
|
||||
for "foobar". This is because the default lookup type is ``exact``, but you
|
||||
probably want to perform an ``icontains`` lookup.
|
||||
|
||||
|
||||
Filter and lookup expression mismatch (in, range, isnull)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's not always appropriate to directly match a filter to its model field's
|
||||
type, as some lookups expect different types of values. This is a commonly
|
||||
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
|
||||
at the following product model:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, null=True)
|
||||
|
||||
Given that ``category`` is optional, it's reasonable to want to enable a search
|
||||
for uncategorized products. The following is an incorrectly configured
|
||||
``isnull`` filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
So what's the issue? While the underlying column type for ``category`` is an
|
||||
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
|
||||
only validates numbers. Filters are not `'expression aware'` and won't change
|
||||
behavior based on their ``lookup_expr``. You should use filters that match the
|
||||
data type of the lookup expression `instead` of the data type underlying the
|
||||
model field. The following would correctly allow you to search for both
|
||||
uncategorized products and products for a set of categories:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
categories = NumberInFilter(name='category', lookup_expr='in')
|
||||
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
More info on constructing ``in`` and ``range`` csv :ref:`filters <base-in-filter>`.
|
||||
|
||||
|
||||
Filtering by empty values
|
||||
-------------------------
|
||||
|
||||
There are a number of cases where you may need to filter by empty or null
|
||||
values. The following are some common solutions to these problems:
|
||||
|
||||
|
||||
Filtering by null values
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As explained in the above "Filter and lookup expression mismatch" section, a
|
||||
common problem is how to correctly filter by null values on a field.
|
||||
|
||||
Solution 1: Using a ``BooleanFilter`` with ``isnull``
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by
|
||||
the FilterSet's automatic filter generation. To do this manually, simply add:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that the filter class is validating the input value. The underlying
|
||||
type of the mode field is not relevant here.
|
||||
|
||||
You may also reverse the logic with the ``exclude`` parameter.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True)
|
||||
|
||||
Solution 2: Using ``ChoiceFilter``'s null choice
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
If you're using a ChoiceFilter, you may also filter by null values by enabling
|
||||
the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
|
||||
:ref:`docs <choice-filter>`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
category = django_filters.ModelChoiceFilter(
|
||||
name='category', lookup_expr='isnull',
|
||||
null_label='Uncategorized',
|
||||
queryset=Category.objects.all(),
|
||||
)
|
||||
|
||||
Solution 3: Combining fields w/ ``MultiValueField``
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
An alternative approach is to use Django's ``MultiValueField`` to manually add
|
||||
in a ``BooleanField`` to handle null values. Proof of concept:
|
||||
https://github.com/carltongibson/django-filter/issues/446
|
||||
|
||||
|
||||
Filtering by an empty string
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's not currently possible to filter by an empty string, since empty values are
|
||||
interpreted as a skipped filter.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield=
|
||||
|
||||
Solution 1: Magic values
|
||||
""""""""""""""""""""""""
|
||||
|
||||
You can override the ``filter()`` method of a filter class to specifically check
|
||||
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield=EMPTY
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyCharFilter(filters.CharFilter):
|
||||
empty_value = 'EMPTY'
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value != self.empty_value:
|
||||
return super(MyCharFilter, self).filter(qs, value)
|
||||
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
Solution 2: Empty string filter
|
||||
"""""""""""""""""""""""""""""""
|
||||
|
||||
It would also be possible to create an empty value filter that exhibits the same
|
||||
behavior as an ``isnull`` filter.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield__isempty=false
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
|
||||
class EmptyStringFilter(filters.BooleanFilter):
|
||||
def filter(self, qs, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
exclude = self.exclude ^ (value is False)
|
||||
method = qs.exclude if exclude else qs.filter
|
||||
|
||||
return method(**{self.name: ""})
|
||||
|
||||
|
||||
class MyFilterSet(filters.FilterSet):
|
||||
myfield__isempty = EmptyStringFilter(name='myfield')
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
|
@ -1,5 +1,6 @@
|
|||
Using django-filter
|
||||
===================
|
||||
===============
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Django-filter provides a simple way to filter down a queryset based on
|
||||
parameters a user provides. Say we have a ``Product`` model and we want to let
|
||||
|
@ -80,82 +81,6 @@ For Django version 1.8, transformed expressions are not supported.
|
|||
.. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups
|
||||
|
||||
|
||||
Common declarative problems
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Below are some of the common problem that occur when declaring filters. It is
|
||||
recommended that you do read this as it provides a more complete understanding
|
||||
on how filters work.
|
||||
|
||||
|
||||
Filter ``name`` and ``lookup_expr`` not configured
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
|
||||
them. By default, if ``name`` is not specified, the filter's name on the
|
||||
filterset class will be used. Additionally, ``lookup_expr`` defaults to
|
||||
``exact``. The following is an example of a misconfigured price filter::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter()
|
||||
|
||||
The filter instance will have a field name of ``price__gt`` and an ``exact``
|
||||
lookup type. Under the hood, this will incorrectly be resolved as::
|
||||
|
||||
Produce.objects.filter(price__gt__exact=value)
|
||||
|
||||
The above will most likely generate a ``FieldError``. The correct configuration
|
||||
would be::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
|
||||
|
||||
|
||||
Missing ``lookup_expr`` for text search filters
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
It's quite common to forget to set the lookup expression for :code:`CharField`
|
||||
and :code:`TextField` and wonder why a search for "foo" does not return results
|
||||
for "foobar". This is because the default lookup type is ``exact``, but you
|
||||
probably want to perform an ``icontains`` lookup.
|
||||
|
||||
|
||||
Filter and lookup expression mismatch (in, range, isnull)
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
It's not always appropriate to directly match a filter to its model field's
|
||||
type, as some lookups expect different types of values. This is a commonly
|
||||
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
|
||||
at the following product model::
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, null=True)
|
||||
|
||||
Given that ``category`` is optional, it's reasonable to want to enable a search
|
||||
for uncategorized products. The following is an incorrectly configured
|
||||
``isnull`` filter::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
So what's the issue? While the underlying column type for ``category`` is an
|
||||
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
|
||||
only validates numbers. Filters are not `'expression aware'` and won't change
|
||||
behavior based on their ``lookup_expr``. You should use filters that match the
|
||||
data type of the lookup expression `instead` of the data type underlying model
|
||||
field. The following would correctly allow you to search for both uncategorized
|
||||
products and products for a set of categories::
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
categories = NumberInFilter(name='category', lookup_expr='in')
|
||||
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
More info on constructing IN and RANGE csv :ref:`filters <base-in-filter>`.
|
||||
|
||||
|
||||
Generating filters with Meta.fields
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -223,7 +148,7 @@ default filters for all the models fields of the same kind using
|
|||
models.BooleanField: {
|
||||
'filter_class': django_filters.BooleanFilter,
|
||||
'extra': lambda f: {
|
||||
'widget': 'forms.CheckboxInput',
|
||||
'widget': forms.CheckboxInput,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -236,11 +161,63 @@ default filters for all the models fields of the same kind using
|
|||
}
|
||||
|
||||
|
||||
Request-based filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``FilterSet`` may be initialized with an optional ``request`` argument. If
|
||||
a request object is passed, then you may access the request during filtering.
|
||||
This allows you to filter by properties on the request, such as the currently
|
||||
logged-in user or the ``Accepts-Languages`` header.
|
||||
|
||||
|
||||
Filtering the primary ``.qs``
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
To filter the primary queryset by the ``request`` object, simply override the
|
||||
``FilterSet.qs`` property. For example, you could filter blog articles to only
|
||||
those that are published and those that are owned by the logged-in user
|
||||
(presumably the author's draft articles).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ArticleFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = [...]
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
parent = super(ArticleFilter, self).qs
|
||||
return parent.filter(is_published=True) \
|
||||
| parent.filter(author=request.user)
|
||||
|
||||
|
||||
Filtering the related queryset for ``ModelChoiceFilter``
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The ``queryset`` argument for ``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter``
|
||||
supports callable behavior. If a callable is passed, it will be invoked with the
|
||||
``request`` as its only argument. This allows you to perform the same kinds of
|
||||
request-based filtering without resorting to overriding ``FilterSet.__init__``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def departments(request):
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
|
||||
Customize filtering with ``Filter.method``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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>`.
|
||||
Note that you may access the filterset's properties, such as the ``request``.
|
||||
|
||||
.. code-block:: python
|
||||
|
|
@ -7,18 +7,28 @@ the more mundane bits of view code. Specifically, it allows users to filter
|
|||
down a queryset based on a model's fields, displaying the form to let them
|
||||
do this.
|
||||
|
||||
Contents:
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
|
||||
guide/install
|
||||
guide/usage
|
||||
guide/rest_framework
|
||||
guide/tips
|
||||
guide/migration
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Reference Documentation
|
||||
|
||||
install
|
||||
usage
|
||||
rest_framework
|
||||
ref/filterset
|
||||
ref/filters
|
||||
ref/fields
|
||||
ref/widgets
|
||||
ref/settings
|
||||
migration
|
||||
tests
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Developer Documentation
|
||||
|
||||
dev/tests
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
Installing django-filter
|
||||
------------------------
|
||||
|
||||
Install with pip:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install django-filter
|
||||
|
||||
And then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
|
@ -1,5 +1,6 @@
|
|||
Fields Reference
|
||||
================
|
||||
===============
|
||||
Field Reference
|
||||
===============
|
||||
|
||||
``IsoDateTimeField``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -10,7 +11,9 @@ Defines a class level attribute ``ISO_8601`` as constant for the format.
|
|||
|
||||
Sets ``input_formats = [ISO_8601]`` — this means that by default ``IsoDateTimeField`` will **only** parse ISO 8601 formated dates.
|
||||
|
||||
You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format. ::
|
||||
You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
f = IsoDateTimeField()
|
||||
f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
================
|
||||
Filter Reference
|
||||
================
|
||||
|
||||
|
@ -22,7 +23,9 @@ fields on a related model. eg, ``manufacturer__name``.
|
|||
~~~~~~~~~
|
||||
|
||||
The label as it will apear in the HTML, analogous to a form field's label
|
||||
argument.
|
||||
argument. If a label is not provided, a verbose label will be generated based
|
||||
on the field ``name`` and the parts of the ``lookup_expr``.
|
||||
(See: :ref:`verbose-lookups-setting`).
|
||||
|
||||
``widget``
|
||||
~~~~~~~~~~
|
||||
|
@ -189,14 +192,15 @@ This filter matches UUID values, used with ``models.UUIDField`` by default.
|
|||
This filter matches a boolean, either ``True`` or ``False``, used with
|
||||
``BooleanField`` and ``NullBooleanField`` by default.
|
||||
|
||||
.. _choice-filter:
|
||||
|
||||
``ChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This filter matches an item of any type by choices, used with any field that
|
||||
has ``choices``.
|
||||
This filter matches values in its ``choices`` argument. The ``choices`` must be
|
||||
explicitly passed when the filter is declared on the ``FilterSet``. For example,
|
||||
|
||||
|
||||
Requires ``choices`` ``kwarg`` to be passed if explicitly declared on the ``FilterSet``. For example::
|
||||
.. code-block:: python
|
||||
|
||||
class User(models.Model):
|
||||
username = models.CharField(max_length=255)
|
||||
|
@ -218,6 +222,20 @@ Requires ``choices`` ``kwarg`` to be passed if explicitly declared on the ``Filt
|
|||
fields = ['status']
|
||||
|
||||
|
||||
``ChoiceFilter`` also has arguments that enable a choice for not filtering, as
|
||||
well as a choice for filtering by ``None`` values. Each of the arguments have a
|
||||
corresponding global setting (:doc:`/ref/settings`).
|
||||
|
||||
* ``empty_label``: The display label to use for the select choice to not filter.
|
||||
The choice may be disabled by setting this argument to ``None``. Defaults to
|
||||
``FILTERS_EMPTY_CHOICE_LABEL``.
|
||||
* ``null_label``: The display label to use for the choice to filter by ``None``
|
||||
values. The choice may be disabled by setting this argument to ``None``.
|
||||
Defaults to ``FILTERS_NULL_CHOICE_LABEL``.
|
||||
* ``null_value``: The special value to match to enable filtering by ``None``
|
||||
values. This value defaults ``FILTERS_NULL_CHOICE_VALUE`` and needs to be
|
||||
a non-empty value (``''``, ``None``, ``[]``, ``()``, ``{}``).
|
||||
|
||||
|
||||
``TypedChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -249,6 +267,8 @@ argument is passed to this class.
|
|||
Multiple choices are represented in the query string by reusing the same key with
|
||||
different values (e.g. ''?status=Regular&status=Admin'').
|
||||
|
||||
``distinct`` defaults to ``True`` as to-many relationships will generally require this.
|
||||
|
||||
Advanced Use: Depending on your application logic, when all or no choices are
|
||||
selected, filtering may be a noop. In this case you may wish to avoid the
|
||||
filtering overhead, particularly of the `distinct` call.
|
||||
|
@ -259,6 +279,13 @@ test.
|
|||
Override `is_noop` if you require a different test for your application.
|
||||
|
||||
|
||||
``TypedMultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Like ``MultipleChoiceFilter``, but in addition accepts the ``coerce`` parameter, as
|
||||
in ``TypedChoiceFilter``.
|
||||
|
||||
|
||||
``DateFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -280,7 +307,8 @@ Matches on a date and time. Used with ``DateTimeField`` by default.
|
|||
``IsoDateTimeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
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.
|
||||
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::
|
||||
|
||||
|
@ -323,6 +351,22 @@ Example::
|
|||
model = Book
|
||||
fields = ['author']
|
||||
|
||||
The ``queryset`` argument also supports callable behavior. If a callable is
|
||||
passed, it will be invoked with ``Filterset.request`` as its only argument.
|
||||
This allows you to easily filter by properties on the request object without
|
||||
having to override the ``FilterSet.__init__``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def departments(request):
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
|
||||
``ModelMultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -332,6 +376,7 @@ 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.
|
||||
Like ``ModelChoiceFilter``, the ``queryset`` argument has callable behavior.
|
||||
|
||||
To use a custom field name for the lookup, you can use ``to_field_name``::
|
||||
|
||||
|
@ -389,15 +434,18 @@ and ``DecimalField`` by default.
|
|||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Filters where a value is between two numerical values, or greater than a minimum or less
|
||||
than a maximum where only one limit value is provided. This filter is designed to work with
|
||||
the Postgres Numerical Range Fields, including `IntegerRangeField`, `BigIntegerRangeField` and `FloatRangeField`,
|
||||
available since Django 1.8. The default widget used is the `RangeField`.
|
||||
than a maximum where only one limit value is provided. This filter is designed to work
|
||||
with the Postgres Numerical Range Fields, including ``IntegerRangeField``,
|
||||
``BigIntegerRangeField`` and ``FloatRangeField`` (available since Django 1.8). The default
|
||||
widget used is the ``RangeField``.
|
||||
|
||||
RangeField lookup_exprs can be used, including `overlap`, `contains`, and `contained_by`. More lookups can be
|
||||
found in the Django docs (https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields).
|
||||
Regular field lookups are available in addition to several containment lookups, including
|
||||
``overlap``, ``contains``, and ``contained_by``. More details in the Django `docs`__.
|
||||
|
||||
If the lower limit value is provided, the filter automatically defaults to `__startswith` as the lookup
|
||||
type and if only the upper limit value is provided, the filter uses `__endswith`.
|
||||
__ https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields
|
||||
|
||||
If the lower limit value is provided, the filter automatically defaults to ``startswith``
|
||||
as the lookup and ``endswith`` if only the upper limit value is provided.
|
||||
|
||||
``RangeFilter``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
@ -619,29 +667,6 @@ Example::
|
|||
assert len(f.qs) == 3
|
||||
|
||||
|
||||
``MethodFilter``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a ``Filter`` that will allow you to run a method that exists on the filter set that
|
||||
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::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by if books are published or not"""
|
||||
published = MethodFilter()
|
||||
|
||||
def filter_published(self, queryset, value):
|
||||
if value:
|
||||
return queryset.filter(published__isnull=False)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['published']
|
||||
|
||||
|
||||
.. _ordering-filter:
|
||||
|
||||
``OrderingFilter``
|
||||
|
@ -718,3 +743,28 @@ 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.
|
||||
|
||||
Adding Custom filter choices
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
If you wish to sort by non-model fields, you'll need to add custom handling to an
|
||||
``OrderingFilter`` subclass. For example, if you want to sort by a computed
|
||||
'relevance' factor, you would need to do something like the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomOrderingFilter(django_filters.OrderingFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomOrderingFilter, self).__init__(*args, **kwargs)
|
||||
self.choices += [
|
||||
('relevance', 'Relevance'),
|
||||
('-relevance', 'Relevance (descending)'),
|
||||
]
|
||||
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value in ['relevance', '-relevance']:
|
||||
# sort queryset by relevance
|
||||
return ...
|
||||
return super(CustomOrderingFilter, self).filter(qs, value)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
FilterSet Guide
|
||||
===============
|
||||
=================
|
||||
FilterSet Options
|
||||
=================
|
||||
|
||||
This document provides a guide on using additional FilterSet features.
|
||||
|
||||
|
@ -9,7 +10,6 @@ Meta options
|
|||
- :ref:`model <model>`
|
||||
- :ref:`fields <fields>`
|
||||
- :ref:`exclude <exclude>`
|
||||
- :ref:`order_by <order-by>`
|
||||
- :ref:`form <form>`
|
||||
- :ref:`together <together>`
|
||||
- filter_overrides
|
||||
|
@ -91,40 +91,6 @@ declared directly on the ``FilterSet``.
|
|||
exclude = ['password']
|
||||
|
||||
|
||||
.. _order-by:
|
||||
|
||||
Ordering using ``order_by``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can allow the user to control ordering by providing the
|
||||
``order_by`` argument in the Filter's Meta class. ``order_by`` can be either a
|
||||
``list`` or ``tuple`` of field names, in which case those are the options, or
|
||||
it can be a ``bool`` which, if True, indicates that all fields that
|
||||
the user can filter on can also be sorted on. An example of ordering using a list::
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price = django_filters.NumberFilter(lookup_expr='lt')
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['price', 'release_date']
|
||||
order_by = ['price']
|
||||
|
||||
If you want to control the display of items in ``order_by``, you can set it to
|
||||
a list or tuple of 2-tuples in the format ``(field_name, display_name)``.
|
||||
This lets you override the displayed names for your ordering fields::
|
||||
|
||||
order_by = (
|
||||
('name', 'Company Name'),
|
||||
('average_rating', 'Stars'),
|
||||
)
|
||||
|
||||
Note that the default query parameter name used for ordering is ``o``. You
|
||||
can override this by setting an ``order_by_field`` attribute on the
|
||||
``FilterSet``'s Meta class to the string value you would like to use.
|
||||
|
||||
|
||||
.. _form:
|
||||
|
||||
Custom Forms using ``form``
|
||||
|
@ -201,29 +167,3 @@ filters for a model field, you can override ``filter_for_lookup()``. Ex::
|
|||
|
||||
# use default behavior otherwise
|
||||
return super(ProductFilter, cls).filter_for_lookup(f, lookup_type)
|
||||
|
||||
|
||||
``get_ordering_field()``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to use a custom widget, or in any other way override the ordering
|
||||
field you can override the ``get_ordering_field()`` method on a ``FilterSet``.
|
||||
This method just needs to return a Form Field.
|
||||
|
||||
Ordering on multiple fields, or other complex orderings can be achieved by
|
||||
overriding the ``FilterSet.get_order_by()`` method. This is passed the selected
|
||||
``order_by`` value, and is expected to return an iterable of values to pass to
|
||||
``QuerySet.order_by``. For example, to sort a ``User`` table by last name, then
|
||||
first name::
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
order_by = (
|
||||
('username', 'Username'),
|
||||
('last_name', 'Last Name')
|
||||
)
|
||||
|
||||
def get_order_by(self, order_value):
|
||||
if order_value == 'last_name':
|
||||
return ['last_name', 'first_name']
|
||||
return super(UserFilter, self).get_order_by(order_value)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
.. _ref-settings:
|
||||
|
||||
==================
|
||||
Available Settings
|
||||
Settings Reference
|
||||
==================
|
||||
|
||||
Here is a list of all available settings of django-filters and their
|
||||
|
@ -9,20 +7,91 @@ default values. All settings are prefixed with ``FILTERS_``, although this
|
|||
is a bit verbose it helps to make it easy to identify these settings.
|
||||
|
||||
|
||||
FILTERS_HELP_TEXT_EXCLUDE
|
||||
FILTERS_EMPTY_CHOICE_LABEL
|
||||
--------------------------
|
||||
|
||||
Default: ``'---------'``
|
||||
|
||||
Set the default value for ``ChoiceFilter.empty_label``. You may disable the empty choice by setting this to ``None``.
|
||||
|
||||
|
||||
FILTERS_NULL_CHOICE_LABEL
|
||||
-------------------------
|
||||
|
||||
Default: ``True``
|
||||
Default: ``None``
|
||||
|
||||
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``.
|
||||
Set the default value for ``ChoiceFilter.null_label``. You may enable the null choice by setting a non-``None`` value.
|
||||
|
||||
.. note:: This setting is pending deprecation.
|
||||
|
||||
FILTERS_HELP_TEXT_FILTER
|
||||
------------------------
|
||||
FILTERS_NULL_CHOICE_VALUE
|
||||
-------------------------
|
||||
|
||||
Default: ``True``
|
||||
Default: ``'null'``
|
||||
|
||||
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``.
|
||||
Set the default value for ``ChoiceFilter.null_value``. You may want to change this value if the default ``'null'`` string conflicts with an actual choice.
|
||||
|
||||
.. note:: This setting is pending deprecation.
|
||||
|
||||
FILTERS_DISABLE_HELP_TEXT
|
||||
-------------------------
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Some filters provide informational ``help_text``. For example, csv-based
|
||||
filters (``filters.BaseCSVFilter``) inform users that "Multiple values may
|
||||
be separated by commas".
|
||||
|
||||
You may set this to ``True`` to disable the ``help_text`` for **all**
|
||||
filters, removing the text from the rendered form's output.
|
||||
|
||||
|
||||
.. _verbose-lookups-setting:
|
||||
|
||||
FILTERS_VERBOSE_LOOKUPS
|
||||
-----------------------
|
||||
|
||||
.. note::
|
||||
|
||||
This is considered an advanced setting and is subject to change.
|
||||
|
||||
Default:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# refer to 'django_filters.conf.DEFAULTS'
|
||||
'VERBOSE_LOOKUPS': {
|
||||
'exact': _(''),
|
||||
'iexact': _(''),
|
||||
'contains': _('contains'),
|
||||
'icontains': _('contains'),
|
||||
...
|
||||
}
|
||||
|
||||
This setting controls the verbose output for generated filter labels. Instead
|
||||
of getting expression parts such as "lt" and "contained_by", the verbose label
|
||||
would contain "is less than" and "is contained by". Verbose output may be
|
||||
disabled by setting this to a falsy value.
|
||||
|
||||
This setting also accepts callables. The callable should not require arguments
|
||||
and should return a dictionary. This is useful for extending or overriding the
|
||||
default terms without having to copy the entire set of terms to your settings.
|
||||
For example, you could add verbose output for "exact" lookups.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
def FILTERS_VERBOSE_LOOKUPS():
|
||||
from django_filters.conf import DEFAULTS
|
||||
|
||||
verbose_lookups = DEFAULTS['VERBOSE_LOOKUPS'].copy()
|
||||
verbose_lookups.update({
|
||||
'exact': 'is equal to',
|
||||
})
|
||||
return verbose_lookups
|
||||
|
||||
|
||||
FILTERS_STRICTNESS
|
||||
------------------
|
||||
|
||||
Default: ``STRICTNESS.RETURN_NO_RESULTS``
|
||||
|
||||
Set the global default for FilterSet :ref:`strictness <strict>`.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
================
|
||||
Widget Reference
|
||||
================
|
||||
|
||||
|
@ -29,7 +30,9 @@ placeholders:
|
|||
|
||||
This widget converts its input into Python's True/False values. It will convert
|
||||
all case variations of ``True`` and ``False`` into the internal Python values.
|
||||
To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``::
|
||||
To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
active = BooleanFilter(widget=BooleanWidget())
|
||||
|
||||
|
@ -53,6 +56,8 @@ This widget is used with ``RangeFilter`` and its subclasses. It generates two
|
|||
form input elements which generally act as start/end values in a range.
|
||||
Under the hood, it is django's ``forms.TextInput`` widget and excepts
|
||||
the same arguments and values. To use it, pass it to ``widget`` argument of
|
||||
a ``RangeField``::
|
||||
a ``RangeField``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'}))
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
Running the django-filter tests
|
||||
===============================
|
||||
|
||||
The easiest way to run the django-filter tests is to check out the source
|
||||
code into a virtualenv, where you can install the test dependencies.
|
||||
django-filter uses a custom test runner to locate all of the tests, so a
|
||||
wrapper script is available to set up and run the test suite.
|
||||
|
||||
.. note::
|
||||
|
||||
The following assumes you have `virtualenv`__ and `git`__ installed.
|
||||
|
||||
__ http://www.virtualenv.org
|
||||
__ http://git-scm.com
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
-----------------------------
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
2
setup.py
2
setup.py
|
@ -6,7 +6,7 @@ f = open('README.rst')
|
|||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
version = '0.15.3'
|
||||
version = '1.0.0'
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
if os.system("pip freeze | grep wheel"):
|
||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||
from django import forms
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
REGULAR = 0
|
||||
|
@ -45,7 +46,7 @@ class SubnetMaskField(models.Field):
|
|||
|
||||
@python_2_unicode_compatible
|
||||
class User(models.Model):
|
||||
username = models.CharField(max_length=255)
|
||||
username = models.CharField(_('username'), max_length=255)
|
||||
first_name = SubCharField(max_length=100)
|
||||
last_name = SubSubCharField(max_length=100)
|
||||
|
||||
|
@ -95,6 +96,7 @@ class Comment(models.Model):
|
|||
|
||||
|
||||
class Article(models.Model):
|
||||
name = models.CharField(verbose_name='title', max_length=200, blank=True)
|
||||
published = models.DateTimeField()
|
||||
author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||
|
||||
|
|
|
@ -336,12 +336,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
<p>
|
||||
<label for="id_decimal">Decimal:</label>
|
||||
<input id="id_decimal" name="decimal" step="any" type="number" />
|
||||
<span class="helptext">Filter</span>
|
||||
</p>
|
||||
<p>
|
||||
<label for="id_date">Date:</label>
|
||||
<input id="id_date" name="date" type="text" />
|
||||
<span class="helptext">Filter</span>
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
|
|
|
@ -10,6 +10,7 @@ INSTALLED_APPS = (
|
|||
'django.contrib.staticfiles',
|
||||
'django.contrib.auth',
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
'tests.rest_framework',
|
||||
'tests',
|
||||
)
|
||||
|
|
|
@ -2,20 +2,77 @@
|
|||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.conf import settings
|
||||
from django_filters import FilterSet, STRICTNESS
|
||||
|
||||
from tests.models import User
|
||||
|
||||
|
||||
class DefaultSettingsTests(TestCase):
|
||||
|
||||
def test_verbose_loookups(self):
|
||||
def test_verbose_lookups(self):
|
||||
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
|
||||
self.assertIn('exact', settings.VERBOSE_LOOKUPS)
|
||||
|
||||
def test_disable_help_text(self):
|
||||
self.assertFalse(settings.DISABLE_HELP_TEXT)
|
||||
|
||||
def test_strictness(self):
|
||||
self.assertEqual(settings.STRICTNESS, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_help_text_filter(self):
|
||||
self.assertTrue(settings.HELP_TEXT_FILTER)
|
||||
|
||||
def test_help_text_exclude(self):
|
||||
self.assertTrue(settings.HELP_TEXT_EXCLUDE)
|
||||
|
||||
def test_empty_choice_label(self):
|
||||
self.assertEqual(settings.EMPTY_CHOICE_LABEL, '---------')
|
||||
|
||||
def test_null_choice_label(self):
|
||||
self.assertIsNone(settings.NULL_CHOICE_LABEL)
|
||||
|
||||
def test_null_choice_value(self):
|
||||
self.assertEqual(settings.NULL_CHOICE_VALUE, 'null')
|
||||
|
||||
|
||||
class StrictnessTests(TestCase):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
def test_settings_default(self):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_ignore(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_return_no_results(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RETURN_NO_RESULTS):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_raise_validation_error(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RAISE_VALIDATION_ERROR):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
|
||||
|
||||
def test_legacy_ignore(self):
|
||||
with override_settings(FILTERS_STRICTNESS=False):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_legacy_return_no_results(self):
|
||||
with override_settings(FILTERS_STRICTNESS=True):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_legacy_raise_validation_error(self):
|
||||
with override_settings(FILTERS_STRICTNESS='RAISE'):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
|
||||
|
||||
def test_legacy_differentiation(self):
|
||||
self.assertNotEqual(STRICTNESS.IGNORE, False)
|
||||
self.assertNotEqual(STRICTNESS.RETURN_NO_RESULTS, True)
|
||||
self.assertNotEqual(STRICTNESS.RAISE_VALIDATION_ERROR, 'RAISE')
|
||||
|
||||
|
||||
class OverrideSettingsTests(TestCase):
|
||||
|
||||
|
|
|
@ -1,718 +0,0 @@
|
|||
|
||||
import functools
|
||||
import warnings
|
||||
import mock
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters import FilterSet
|
||||
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
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class FilterSetContainerDeprecationTests(TestCase):
|
||||
|
||||
def test__iter__notification(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
[obj for obj in UserFilter()]
|
||||
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
||||
def test__getitem__notification(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
with self.assertRaises(IndexError):
|
||||
UserFilter()[0]
|
||||
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
||||
def test__len__notification(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
len(UserFilter())
|
||||
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
||||
def test__count__notification(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
UserFilter().count()
|
||||
|
||||
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:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertIn("Not setting Meta.fields with Meta.model is undocumented behavior", str(w[-1].message))
|
||||
|
||||
def test_fields_is_none(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = None
|
||||
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertIn("Setting 'Meta.fields = None' is undocumented behavior", str(w[-1].message))
|
||||
|
||||
def test_fields_not_set_ignore_unknown(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = NetworkSetting
|
||||
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
self.assertIn("Not setting Meta.fields with Meta.model is undocumented behavior", str(w[-1].message))
|
||||
|
||||
self.assertNotIn('mask', F.base_filters.keys())
|
||||
|
||||
def test_fields_not_set_with_override(self):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
class F(FilterSet):
|
||||
|
||||
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))
|
|
@ -20,24 +20,21 @@ from django_filters.filters import ChoiceFilter
|
|||
from django_filters.filters import DateRangeFilter
|
||||
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 MultipleChoiceFilter
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filters import TypedMultipleChoiceFilter
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import NumberFilter
|
||||
from django_filters.filters import OrderingFilter
|
||||
from django_filters.filters import RangeFilter
|
||||
from django_filters.filters import TimeRangeFilter
|
||||
# from django_filters.widgets import LinkWidget
|
||||
from django_filters.exceptions import FieldLookupError
|
||||
|
||||
from .models import User
|
||||
from .models import Comment
|
||||
from .models import Book
|
||||
# from .models import Restaurant
|
||||
from .models import Article
|
||||
# from .models import NetworkSetting
|
||||
# from .models import SubnetMaskField
|
||||
from .models import Company
|
||||
from .models import Location
|
||||
from .models import Account
|
||||
|
@ -129,12 +126,20 @@ class BooleanFilterTests(TestCase):
|
|||
|
||||
class ChoiceFilterTests(TestCase):
|
||||
|
||||
def test_filtering(self):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
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)
|
||||
|
||||
Article.objects.create(author_id=1, published=now())
|
||||
Article.objects.create(author_id=2, published=now())
|
||||
Article.objects.create(author_id=3, published=now())
|
||||
Article.objects.create(author_id=4, published=now())
|
||||
Article.objects.create(author_id=None, published=now())
|
||||
|
||||
def test_filtering(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -160,11 +165,6 @@ class ChoiceFilterTests(TestCase):
|
|||
|
||||
If you explicitly declare ChoiceFilter fields you **MUST** pass `choices`.
|
||||
"""
|
||||
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)
|
||||
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(choices=STATUS_CHOICES)
|
||||
|
||||
|
@ -186,6 +186,38 @@ class ChoiceFilterTests(TestCase):
|
|||
f = F({'status': '0'})
|
||||
self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username, False)
|
||||
|
||||
def test_filtering_on_empty_choice(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status']
|
||||
|
||||
f = F({'status': ''})
|
||||
self.assertQuerysetEqual(f.qs,
|
||||
['aaron', 'alex', 'jacob', 'carl'],
|
||||
lambda o: o.username, False)
|
||||
|
||||
def test_filtering_on_null_choice(self):
|
||||
choices = [(u.pk, str(u)) for u in User.objects.order_by('id')]
|
||||
|
||||
class F(FilterSet):
|
||||
author = ChoiceFilter(
|
||||
choices=choices,
|
||||
null_value='null',
|
||||
null_label='NULL',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['author']
|
||||
|
||||
# sanity check to make sure the filter is setup correctly
|
||||
f = F({'author': '1'})
|
||||
self.assertQuerysetEqual(f.qs, ['alex'], lambda o: str(o.author), False)
|
||||
|
||||
f = F({'author': 'null'})
|
||||
self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False)
|
||||
|
||||
|
||||
class MultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -221,6 +253,41 @@ class MultipleChoiceFilterTests(TestCase):
|
|||
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilterTests(TestCase):
|
||||
|
||||
def test_filtering(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)
|
||||
|
||||
|
||||
class F(FilterSet):
|
||||
status = TypedMultipleChoiceFilter(choices=STATUS_CHOICES, coerce=lambda x: x[0:2])
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status']
|
||||
|
||||
qs = User.objects.all().order_by('username')
|
||||
f = F(queryset=qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['aa', 'ja', 'al', 'ca'],
|
||||
lambda o: o.username[0:2], False)
|
||||
|
||||
f = F({'status': ['0']}, queryset=qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['ca'], lambda o: o.username[0:2])
|
||||
|
||||
f = F({'status': ['0', '1']}, queryset=qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['al', 'ca'], lambda o: o.username[0:2])
|
||||
|
||||
f = F({'status': ['0', '1', '2']}, queryset=qs)
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['aa', 'al', 'ca', 'ja'], lambda o: o.username[0:2])
|
||||
|
||||
|
||||
class DateFilterTests(TestCase):
|
||||
|
||||
def test_filtering(self):
|
||||
|
@ -418,6 +485,34 @@ class ModelChoiceFilterTests(TestCase):
|
|||
f = F({'author': jacob.pk}, queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False)
|
||||
|
||||
def test_callable_queryset(self):
|
||||
# Sanity check for callable queryset arguments.
|
||||
# Ensure that nothing is improperly cached
|
||||
User.objects.create(username='alex')
|
||||
jacob = User.objects.create(username='jacob')
|
||||
aaron = User.objects.create(username='aaron')
|
||||
|
||||
def users(request):
|
||||
return User.objects.filter(pk__lt=request.user.pk)
|
||||
|
||||
class F(FilterSet):
|
||||
author = ModelChoiceFilter(name='author', queryset=users)
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['author']
|
||||
|
||||
qs = Comment.objects.all()
|
||||
request = mock.Mock()
|
||||
|
||||
request.user = jacob
|
||||
f = F(queryset=qs, request=request).filters['author'].field
|
||||
self.assertQuerysetEqual(f.queryset, [1], lambda o: o.pk, False)
|
||||
|
||||
request.user = aaron
|
||||
f = F(queryset=qs, request=request).filters['author'].field
|
||||
self.assertQuerysetEqual(f.queryset, [1, 2], lambda o: o.pk, False)
|
||||
|
||||
|
||||
class ModelMultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -1644,7 +1739,7 @@ class MiscFilterSetTests(TestCase):
|
|||
model = User
|
||||
fields = ['account']
|
||||
|
||||
qs = mock.MagicMock()
|
||||
qs = mock.NonCallableMagicMock()
|
||||
f = F({'account': 'jdoe'}, queryset=qs)
|
||||
result = f.qs
|
||||
self.assertNotEqual(qs, result)
|
||||
|
@ -1665,6 +1760,8 @@ class MiscFilterSetTests(TestCase):
|
|||
self.assertQuerysetEqual(f.qs, [], lambda o: o.pk)
|
||||
|
||||
def test_filter_with_initial(self):
|
||||
# Initial values are a form presentation option - the FilterSet should
|
||||
# not use an initial value as a default value to filter by.
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(choices=STATUS_CHOICES, initial=1)
|
||||
|
||||
|
@ -1673,8 +1770,10 @@ class MiscFilterSetTests(TestCase):
|
|||
fields = ['status']
|
||||
|
||||
qs = User.objects.all()
|
||||
users = ['alex', 'jacob', 'aaron', 'carl']
|
||||
|
||||
f = F(queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, ['alex'], lambda o: o.username)
|
||||
self.assertQuerysetEqual(f.qs.order_by('pk'), users, lambda o: o.username)
|
||||
|
||||
f = F({'status': 0}, queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, ['carl'], lambda o: o.username)
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, time, timedelta, datetime
|
||||
import inspect
|
||||
import mock
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import translation
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django_filters import filters, widgets
|
||||
from django_filters.fields import (
|
||||
|
@ -24,6 +27,7 @@ from django_filters.filters import (
|
|||
BooleanFilter,
|
||||
ChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
TypedMultipleChoiceFilter,
|
||||
DateFilter,
|
||||
DateTimeFilter,
|
||||
TimeFilter,
|
||||
|
@ -48,6 +52,28 @@ from django_filters.filters import (
|
|||
from tests.models import Book, User
|
||||
|
||||
|
||||
class ModuleImportTests(TestCase):
|
||||
def is_filter(self, name, value):
|
||||
return (
|
||||
isinstance(value, type) and issubclass(value, Filter)
|
||||
)
|
||||
|
||||
def test_imports(self):
|
||||
# msg = "Expected `filters.%s` to be imported in `filters.__all__`"
|
||||
filter_classes = [
|
||||
key for key, value
|
||||
in inspect.getmembers(filters)
|
||||
if isinstance(value, type) and issubclass(value, Filter)
|
||||
]
|
||||
|
||||
# sanity check
|
||||
self.assertIn('Filter', filter_classes)
|
||||
self.assertIn('BooleanFilter', filter_classes)
|
||||
|
||||
for f in filter_classes:
|
||||
self.assertIn(f, filters.__all__)
|
||||
|
||||
|
||||
class FilterTests(TestCase):
|
||||
|
||||
def test_creation(self):
|
||||
|
@ -64,27 +90,6 @@ class FilterTests(TestCase):
|
|||
f = Filter()
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.Field)
|
||||
self.assertEqual(field.help_text, 'Filter')
|
||||
|
||||
def test_field_with_exclusion(self):
|
||||
f = Filter(exclude=True)
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.Field)
|
||||
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
||||
|
||||
@override_settings(FILTERS_HELP_TEXT_FILTER=False)
|
||||
def test_default_field_settings(self):
|
||||
f = Filter()
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.Field)
|
||||
self.assertEqual(field.help_text, '')
|
||||
|
||||
@override_settings(FILTERS_HELP_TEXT_EXCLUDE=False)
|
||||
def test_field_with_exclusion_settings(self):
|
||||
f = Filter(exclude=True)
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.Field)
|
||||
self.assertEqual(field.help_text, '')
|
||||
|
||||
def test_field_with_single_lookup_expr(self):
|
||||
f = Filter(lookup_expr='iexact')
|
||||
|
@ -102,7 +107,6 @@ class FilterTests(TestCase):
|
|||
f = Filter(lookup_expr=None, exclude=True)
|
||||
field = f.field
|
||||
self.assertIsInstance(field, LookupTypeField)
|
||||
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
||||
|
||||
def test_field_with_list_lookup_expr(self):
|
||||
f = Filter(lookup_expr=('istartswith', 'iendswith'))
|
||||
|
@ -119,8 +123,7 @@ class FilterTests(TestCase):
|
|||
f.field
|
||||
mocked.assert_called_once_with(required=False,
|
||||
label='somelabel',
|
||||
widget='somewidget',
|
||||
help_text=mock.ANY)
|
||||
widget='somewidget')
|
||||
|
||||
def test_field_extra_params(self):
|
||||
with mock.patch.object(Filter, 'field_class',
|
||||
|
@ -128,8 +131,8 @@ class FilterTests(TestCase):
|
|||
f = Filter(someattr='someattr')
|
||||
f.field
|
||||
mocked.assert_called_once_with(required=mock.ANY,
|
||||
label=mock.ANY, widget=mock.ANY,
|
||||
help_text=mock.ANY,
|
||||
label=mock.ANY,
|
||||
widget=mock.ANY,
|
||||
someattr='someattr')
|
||||
|
||||
def test_field_with_required_filter(self):
|
||||
|
@ -137,8 +140,9 @@ class FilterTests(TestCase):
|
|||
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)
|
||||
|
||||
def test_filtering(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
|
@ -212,16 +216,6 @@ class FilterTests(TestCase):
|
|||
result = qs.distinct.assert_called_once_with()
|
||||
self.assertNotEqual(qs, result)
|
||||
|
||||
def test_lookup_type_deprecation(self):
|
||||
"""
|
||||
Make sure user is alerted when using deprecated ``lookup_type``.
|
||||
"""
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
Filter(lookup_type='exact')
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
|
||||
|
||||
|
||||
class CustomFilterWithBooleanCheckTests(TestCase):
|
||||
|
||||
|
@ -316,6 +310,82 @@ class ChoiceFilterTests(TestCase):
|
|||
field = f.field
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
|
||||
def test_empty_choice(self):
|
||||
# default value
|
||||
f = ChoiceFilter(choices=[('a', 'a')])
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set value, allow blank label
|
||||
f = ChoiceFilter(choices=[('a', 'a')], empty_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# disable empty choice w/ None
|
||||
f = ChoiceFilter(choices=[('a', 'a')], empty_label=None)
|
||||
self.assertEqual(f.field.choices, [
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
def test_null_choice(self):
|
||||
# default is to be disabled
|
||||
f = ChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set label, allow blank label
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('null', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set null value
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('NULL', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# explicitly disable
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_label=None)
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
@override_settings(
|
||||
FILTERS_EMPTY_CHOICE_LABEL='EMPTY LABEL',
|
||||
FILTERS_NULL_CHOICE_LABEL='NULL LABEL',
|
||||
FILTERS_NULL_CHOICE_VALUE='NULL VALUE', )
|
||||
def test_settings_overrides(self):
|
||||
f = ChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', 'EMPTY LABEL'),
|
||||
('NULL VALUE', 'NULL LABEL'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
def test_callable_choices(self):
|
||||
def choices():
|
||||
yield ('a', 'a')
|
||||
yield ('b', 'b')
|
||||
|
||||
f = ChoiceFilter(choices=choices)
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
('b', 'b'),
|
||||
])
|
||||
|
||||
|
||||
class MultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -454,6 +524,143 @@ class MultipleChoiceFilterTests(TestCase):
|
|||
expected_pks, item[1], item[0]))
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilterTests(TestCase):
|
||||
|
||||
def test_default_field(self):
|
||||
f = TypedMultipleChoiceFilter()
|
||||
field = f.field
|
||||
self.assertIsInstance(field, forms.TypedMultipleChoiceField)
|
||||
|
||||
def test_filtering_requires_name(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
f = TypedMultipleChoiceFilter()
|
||||
with self.assertRaises(TypeError):
|
||||
f.filter(qs, ['value'])
|
||||
|
||||
def test_conjoined_default_value(self):
|
||||
f = TypedMultipleChoiceFilter()
|
||||
self.assertFalse(f.conjoined)
|
||||
|
||||
def test_conjoined_true(self):
|
||||
f = TypedMultipleChoiceFilter(conjoined=True)
|
||||
self.assertTrue(f.conjoined)
|
||||
|
||||
def test_filtering(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
f = TypedMultipleChoiceFilter(name='somefield')
|
||||
with mock.patch('django_filters.filters.Q') as mockQclass:
|
||||
mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock()
|
||||
mockQclass.side_effect = [mockQ1, mockQ2]
|
||||
|
||||
f.filter(qs, ['value'])
|
||||
|
||||
self.assertEqual(mockQclass.call_args_list,
|
||||
[mock.call(), mock.call(somefield='value')])
|
||||
mockQ1.__ior__.assert_called_once_with(mockQ2)
|
||||
qs.filter.assert_called_once_with(mockQ1.__ior__.return_value)
|
||||
qs.filter.return_value.distinct.assert_called_once_with()
|
||||
|
||||
def test_filtering_exclude(self):
|
||||
qs = mock.Mock(spec=['exclude'])
|
||||
f = TypedMultipleChoiceFilter(name='somefield', exclude=True)
|
||||
with mock.patch('django_filters.filters.Q') as mockQclass:
|
||||
mockQ1, mockQ2 = mock.MagicMock(), mock.MagicMock()
|
||||
mockQclass.side_effect = [mockQ1, mockQ2]
|
||||
|
||||
f.filter(qs, ['value'])
|
||||
|
||||
self.assertEqual(mockQclass.call_args_list,
|
||||
[mock.call(), mock.call(somefield='value')])
|
||||
mockQ1.__ior__.assert_called_once_with(mockQ2)
|
||||
qs.exclude.assert_called_once_with(mockQ1.__ior__.return_value)
|
||||
qs.exclude.return_value.distinct.assert_called_once_with()
|
||||
|
||||
def test_filtering_on_required_skipped_when_len_of_value_is_len_of_field_choices(self):
|
||||
qs = mock.Mock(spec=[])
|
||||
f = TypedMultipleChoiceFilter(name='somefield', required=True)
|
||||
f.always_filter = False
|
||||
result = f.filter(qs, [])
|
||||
self.assertEqual(len(f.field.choices), 0)
|
||||
self.assertEqual(qs, result)
|
||||
|
||||
f.field.choices = ['some', 'values', 'here']
|
||||
result = f.filter(qs, ['some', 'values', 'here'])
|
||||
self.assertEqual(qs, result)
|
||||
|
||||
result = f.filter(qs, ['other', 'values', 'there'])
|
||||
self.assertEqual(qs, result)
|
||||
|
||||
def test_filtering_skipped_with_empty_list_value_and_some_choices(self):
|
||||
qs = mock.Mock(spec=[])
|
||||
f = TypedMultipleChoiceFilter(name='somefield')
|
||||
f.field.choices = ['some', 'values', 'here']
|
||||
result = f.filter(qs, [])
|
||||
self.assertEqual(qs, result)
|
||||
|
||||
def test_filter_conjoined_true(self):
|
||||
"""Tests that a filter with `conjoined=True` returns objects that
|
||||
have all the values included in `value`. For example filter
|
||||
users that have all of this books.
|
||||
|
||||
"""
|
||||
book_kwargs = {'price': 1, 'average_rating': 1}
|
||||
books = []
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
books.append(Book.objects.create(**book_kwargs))
|
||||
|
||||
user1 = User.objects.create()
|
||||
user2 = User.objects.create()
|
||||
user3 = User.objects.create()
|
||||
user4 = User.objects.create()
|
||||
user5 = User.objects.create()
|
||||
|
||||
user1.favorite_books.add(books[0], books[1])
|
||||
user2.favorite_books.add(books[0], books[1], books[2])
|
||||
user3.favorite_books.add(books[1], books[2])
|
||||
user4.favorite_books.add(books[2], books[3])
|
||||
user5.favorite_books.add(books[4], books[5])
|
||||
|
||||
filter_list = (
|
||||
((books[0].pk, books[0].pk), # values
|
||||
[1, 2]), # list of user.pk that have `value` books
|
||||
((books[1].pk, books[1].pk),
|
||||
[1, 2, 3]),
|
||||
((books[2].pk, books[2].pk),
|
||||
[2, 3, 4]),
|
||||
((books[3].pk, books[3].pk),
|
||||
[4, ]),
|
||||
((books[4].pk, books[4].pk),
|
||||
[5, ]),
|
||||
((books[0].pk, books[1].pk),
|
||||
[1, 2]),
|
||||
((books[0].pk, books[2].pk),
|
||||
[2, ]),
|
||||
((books[1].pk, books[2].pk),
|
||||
[2, 3]),
|
||||
((books[2].pk, books[3].pk),
|
||||
[4, ]),
|
||||
((books[4].pk, books[5].pk),
|
||||
[5, ]),
|
||||
((books[3].pk, books[4].pk),
|
||||
[]),
|
||||
)
|
||||
users = User.objects.all()
|
||||
|
||||
for item in filter_list:
|
||||
f = TypedMultipleChoiceFilter(name='favorite_books__pk', conjoined=True)
|
||||
queryset = f.filter(users, item[0])
|
||||
expected_pks = [c[0] for c in queryset.values_list('pk')]
|
||||
self.assertListEqual(
|
||||
expected_pks,
|
||||
item[1],
|
||||
'Lists Differ: {0} != {1} for case {2}'.format(
|
||||
expected_pks, item[1], item[0]))
|
||||
|
||||
|
||||
class DateFilterTests(TestCase):
|
||||
|
||||
def test_default_field(self):
|
||||
|
@ -500,6 +707,33 @@ class ModelChoiceFilterTests(TestCase):
|
|||
self.assertIsInstance(field, forms.ModelChoiceField)
|
||||
self.assertEqual(field.queryset, qs)
|
||||
|
||||
def test_callable_queryset(self):
|
||||
request = mock.NonCallableMock(spec=[])
|
||||
qs = mock.NonCallableMock(spec=[])
|
||||
|
||||
qs_callable = mock.Mock(return_value=qs)
|
||||
|
||||
f = ModelChoiceFilter(queryset=qs_callable)
|
||||
f.parent = mock.Mock(request=request)
|
||||
field = f.field
|
||||
|
||||
qs_callable.assert_called_with(request)
|
||||
self.assertEqual(field.queryset, qs)
|
||||
|
||||
def test_get_queryset_override(self):
|
||||
request = mock.NonCallableMock(spec=[])
|
||||
qs = mock.NonCallableMock(spec=[])
|
||||
|
||||
class F(ModelChoiceFilter):
|
||||
get_queryset = mock.create_autospec(ModelChoiceFilter.get_queryset, return_value=qs)
|
||||
|
||||
f = F()
|
||||
f.parent = mock.Mock(request=request)
|
||||
field = f.field
|
||||
|
||||
f.get_queryset.assert_called_with(f, request)
|
||||
self.assertEqual(field.queryset, qs)
|
||||
|
||||
|
||||
class ModelMultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -530,6 +764,19 @@ class ModelMultipleChoiceFilterTests(TestCase):
|
|||
self.assertEqual(list(f.filter(qs, ['Firstname'])), [user])
|
||||
self.assertEqual(list(f.filter(qs, [user])), [user])
|
||||
|
||||
def test_callable_queryset(self):
|
||||
request = mock.NonCallableMock(spec=[])
|
||||
qs = mock.NonCallableMock(spec=[])
|
||||
|
||||
qs_callable = mock.Mock(return_value=qs)
|
||||
|
||||
f = ModelMultipleChoiceFilter(queryset=qs_callable)
|
||||
f.parent = mock.Mock(request=request)
|
||||
field = f.field
|
||||
|
||||
qs_callable.assert_called_with(request)
|
||||
self.assertEqual(field.queryset, qs)
|
||||
|
||||
|
||||
class NumberFilterTests(TestCase):
|
||||
|
||||
|
@ -1036,6 +1283,7 @@ class OrderingFilterTests(TestCase):
|
|||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
('', '---------'),
|
||||
('a', 'A'),
|
||||
('b', 'B'),
|
||||
))
|
||||
|
@ -1046,6 +1294,7 @@ class OrderingFilterTests(TestCase):
|
|||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
('', '---------'),
|
||||
('c', 'C'),
|
||||
('-c', 'C (descending)'),
|
||||
('d', 'D'),
|
||||
|
@ -1059,12 +1308,28 @@ class OrderingFilterTests(TestCase):
|
|||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
('', '---------'),
|
||||
('c', 'foo'),
|
||||
('-c', 'foo (descending)'),
|
||||
('d', 'D'),
|
||||
('-d', 'D (descending)'),
|
||||
))
|
||||
|
||||
def test_field_labels_descending(self):
|
||||
f = OrderingFilter(
|
||||
fields=['username'],
|
||||
field_labels={
|
||||
'username': 'BLABLA',
|
||||
'-username': 'XYZXYZ',
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('username', 'BLABLA'),
|
||||
('-username', 'XYZXYZ'),
|
||||
])
|
||||
|
||||
def test_normalize_fields(self):
|
||||
f = OrderingFilter.normalize_fields
|
||||
O = OrderedDict
|
||||
|
@ -1104,3 +1369,31 @@ class OrderingFilterTests(TestCase):
|
|||
|
||||
self.assertIsInstance(widget, widgets.BaseCSVWidget)
|
||||
self.assertIsInstance(widget, forms.Select)
|
||||
|
||||
def test_translation_sanity(self):
|
||||
with translation.override('pl'):
|
||||
self.assertEqual(_('Username'), 'Nazwa użytkownika')
|
||||
self.assertEqual(_('%s (descending)') % _('Username'), 'Nazwa użytkownika (malejąco)')
|
||||
|
||||
def test_translation_default_label(self):
|
||||
with translation.override('pl'):
|
||||
f = OrderingFilter(fields=['username'])
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('username', 'Nazwa użytkownika'),
|
||||
('-username', 'Nazwa użytkownika (malejąco)'),
|
||||
])
|
||||
|
||||
def test_translation_override_label(self):
|
||||
with translation.override('pl'):
|
||||
f = OrderingFilter(
|
||||
fields=['username'],
|
||||
field_labels={'username': 'BLABLA'},
|
||||
)
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
('', '---------'),
|
||||
('username', 'BLABLA'),
|
||||
('-username', 'BLABLA (malejąco)'),
|
||||
])
|
||||
|
|
|
@ -5,8 +5,9 @@ import unittest
|
|||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.constants import STRICTNESS
|
||||
from django_filters.filterset import FilterSet
|
||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
||||
from django_filters.filters import Filter
|
||||
|
@ -320,6 +321,39 @@ class FilterSetClassCreationTests(TestCase):
|
|||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'price', 'average_rating'])
|
||||
|
||||
def test_model_no_fields_or_exclude(self):
|
||||
with self.assertRaises(AssertionError) as excinfo:
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
|
||||
self.assertIn(
|
||||
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'",
|
||||
str(excinfo.exception)
|
||||
)
|
||||
|
||||
def test_model_fields_empty(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = []
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 0)
|
||||
self.assertListEqual(list(F.base_filters), [])
|
||||
|
||||
def test_model_exclude_empty(self):
|
||||
# equivalent to fields = '__all__'
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
exclude = []
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'price', 'average_rating'])
|
||||
|
||||
def test_declared_and_model_derived(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
@ -394,9 +428,12 @@ class FilterSetClassCreationTests(TestCase):
|
|||
class Meta:
|
||||
model = Book
|
||||
fields = ('username', 'price', 'other', 'another')
|
||||
self.assertEqual(excinfo.exception.args, (
|
||||
"Meta.fields contains a field that isn't defined "
|
||||
"on this FilterSet: other",))
|
||||
|
||||
self.assertEqual(
|
||||
str(excinfo.exception),
|
||||
"'Meta.fields' contains fields that are not defined on this FilterSet: "
|
||||
"other, another"
|
||||
)
|
||||
|
||||
def test_meta_fields_dictionary_containing_unknown(self):
|
||||
with self.assertRaises(TypeError):
|
||||
|
@ -495,6 +532,16 @@ class FilterSetClassCreationTests(TestCase):
|
|||
|
||||
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
|
||||
|
||||
def test_custom_declared_field_no_warning(self):
|
||||
class F(FilterSet):
|
||||
mask = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = NetworkSetting
|
||||
fields = ['mask']
|
||||
|
||||
self.assertEqual(list(F.base_filters.keys()), ['mask'])
|
||||
|
||||
def test_filterset_for_proxy_model(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
|
@ -562,6 +609,59 @@ class FilterSetInstantiationTests(TestCase):
|
|||
f = F(queryset=m)
|
||||
self.assertEqual(f.queryset, m)
|
||||
|
||||
def test_creating_with_request(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
m = mock.Mock()
|
||||
f = F(request=m)
|
||||
self.assertEqual(f.request, m)
|
||||
|
||||
|
||||
class FilterSetStrictnessTests(TestCase):
|
||||
|
||||
def test_settings_default(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
# Ensure default is not IGNORE
|
||||
self.assertEqual(F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
# override and test
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
|
||||
self.assertEqual(F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_meta_value(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
strict = STRICTNESS.IGNORE
|
||||
|
||||
self.assertEqual(F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_init_default(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
strict = STRICTNESS.IGNORE
|
||||
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
self.assertEqual(F(strict=strict).strict, strict)
|
||||
|
||||
def test_legacy_value(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
self.assertEqual(F(strict=False).strict, STRICTNESS.IGNORE)
|
||||
|
||||
|
||||
class FilterSetTogetherTests(TestCase):
|
||||
|
||||
|
@ -642,6 +742,20 @@ class FilterMethodTests(TestCase):
|
|||
self.assertEqual(f.filters['f'].filter.method, filter_f)
|
||||
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
|
||||
|
||||
def test_request_available_during_method_called(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
|
||||
def filter_f(self, qs, name, value):
|
||||
# call mock request object to prove self.request can be accessed
|
||||
self.request()
|
||||
|
||||
m = mock.Mock()
|
||||
f = F({}, queryset=User.objects.all(), request=m)
|
||||
# call the filter
|
||||
f.filters['f'].filter.method(User.objects.all(), 'f', '')
|
||||
m.assert_called_once_with()
|
||||
|
||||
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
|
||||
|
@ -664,6 +778,24 @@ class FilterMethodTests(TestCase):
|
|||
self.assertIn('parent', str(w.exception))
|
||||
self.assertIn('filter_f', str(w.exception))
|
||||
|
||||
def test_method_self_is_parent(self):
|
||||
# Ensure the method isn't 're-parented' on the `FilterMethod` helper class.
|
||||
# Filter methods should have access to the filterset's properties.
|
||||
request = mock.Mock()
|
||||
|
||||
class F(FilterSet):
|
||||
f = CharFilter(method='filter_f')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
def filter_f(inner_self, qs, name, value):
|
||||
self.assertIsInstance(inner_self, F)
|
||||
self.assertIs(inner_self.request, request)
|
||||
|
||||
F({'f': 'foo'}, request=request, queryset=User.objects.all()).qs
|
||||
|
||||
def test_method_unresolvable(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
|
@ -704,7 +836,6 @@ class FilterMethodTests(TestCase):
|
|||
self.assertIs(f.filter, TestFilter.filter)
|
||||
|
||||
|
||||
@unittest.skip('TODO: remove when relevant deprecations have been completed')
|
||||
class MiscFilterSetTests(TestCase):
|
||||
|
||||
def test_no__getitem__(self):
|
||||
|
|
|
@ -2,7 +2,7 @@ from __future__ import absolute_import
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.filterset import FilterSet
|
||||
from django_filters.filters import CharFilter
|
||||
|
@ -66,8 +66,10 @@ class FilterSetFormTests(TestCase):
|
|||
f = F().form
|
||||
self.assertEqual(len(f.fields), 1)
|
||||
self.assertIn('status', f.fields)
|
||||
self.assertEqual(sorted(f.fields['status'].choices),
|
||||
sorted(STATUS_CHOICES))
|
||||
self.assertSequenceEqual(
|
||||
f.fields['status'].choices,
|
||||
(('', '---------'), ) + STATUS_CHOICES
|
||||
)
|
||||
|
||||
def test_form_fields_exclusion(self):
|
||||
class F(FilterSet):
|
||||
|
@ -78,12 +80,36 @@ class FilterSetFormTests(TestCase):
|
|||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['title'].help_text, "This is an exclusion filter")
|
||||
self.assertEqual(f.fields['title'].label, "Exclude title")
|
||||
|
||||
def test_complex_form_fields(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter(label='Filter for users with username')
|
||||
exclude_username = CharFilter(name='username', lookup_expr='iexact', exclude=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
'status': ['exact', 'lt', 'gt'],
|
||||
'favorite_books__title': ['iexact', 'in'],
|
||||
'manager_of__users__username': ['exact'],
|
||||
}
|
||||
|
||||
fields = F().form.fields
|
||||
self.assertEqual(fields['username'].label, 'Filter for users with username')
|
||||
self.assertEqual(fields['exclude_username'].label, 'Exclude username')
|
||||
self.assertEqual(fields['status'].label, 'Status')
|
||||
self.assertEqual(fields['status__lt'].label, 'Status is less than')
|
||||
self.assertEqual(fields['status__gt'].label, 'Status is greater than')
|
||||
self.assertEqual(fields['favorite_books__title__iexact'].label, 'Favorite books title')
|
||||
self.assertEqual(fields['favorite_books__title__in'].label, 'Favorite books title is in')
|
||||
self.assertEqual(fields['manager_of__users__username'].label, 'Manager of users username')
|
||||
|
||||
def test_form_fields_using_widget(self):
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(widget=forms.RadioSelect,
|
||||
choices=STATUS_CHOICES)
|
||||
choices=STATUS_CHOICES,
|
||||
empty_label=None)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -93,8 +119,10 @@ class FilterSetFormTests(TestCase):
|
|||
self.assertEqual(len(f.fields), 2)
|
||||
self.assertIn('status', f.fields)
|
||||
self.assertIn('username', f.fields)
|
||||
self.assertEqual(sorted(f.fields['status'].choices),
|
||||
sorted(STATUS_CHOICES))
|
||||
self.assertSequenceEqual(
|
||||
f.fields['status'].choices,
|
||||
STATUS_CHOICES
|
||||
)
|
||||
self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect)
|
||||
|
||||
def test_form_field_with_custom_label(self):
|
||||
|
@ -118,8 +146,8 @@ class FilterSetFormTests(TestCase):
|
|||
fields = ('book_title',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['book_title'].label, None)
|
||||
self.assertEqual(f['book_title'].label, 'Book title')
|
||||
self.assertEqual(f.fields['book_title'].label, "Title")
|
||||
self.assertEqual(f['book_title'].label, "Title")
|
||||
|
||||
def test_form_field_with_manual_name_and_label(self):
|
||||
class F(FilterSet):
|
||||
|
@ -180,3 +208,24 @@ class FilterSetFormTests(TestCase):
|
|||
self.assertEqual(
|
||||
list(f.fields['manager'].choices), [('', '---------'), (3, 'manager')]
|
||||
)
|
||||
|
||||
def test_disabled_help_text(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = {
|
||||
# 'in' lookups are CSV-based, which have a `help_text`.
|
||||
'title': ['in']
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
F().form.fields['title__in'].help_text,
|
||||
'Multiple values may be separated by commas.'
|
||||
)
|
||||
|
||||
with override_settings(FILTERS_DISABLE_HELP_TEXT=True):
|
||||
|
||||
self.assertEqual(
|
||||
F().form.fields['title__in'].help_text,
|
||||
''
|
||||
)
|
||||
|
|
|
@ -2,11 +2,16 @@
|
|||
import unittest
|
||||
|
||||
import django
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.utils.functional import Promise
|
||||
|
||||
from django_filters.utils import get_model_field, resolve_field
|
||||
from django_filters.utils import (
|
||||
get_field_parts, get_model_field, resolve_field,
|
||||
verbose_field_name, verbose_lookup_expr, label_for_filter
|
||||
)
|
||||
from django_filters.exceptions import FieldLookupError
|
||||
|
||||
from .models import User
|
||||
|
@ -16,6 +21,34 @@ from .models import HiredWorker
|
|||
from .models import Business
|
||||
|
||||
|
||||
class GetFieldPartsTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
parts = get_field_parts(User, 'username')
|
||||
|
||||
self.assertEqual(len(parts), 1)
|
||||
self.assertIsInstance(parts[0], models.CharField)
|
||||
|
||||
def test_non_existent_field(self):
|
||||
result = get_model_field(User, 'unknown__name')
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_forwards_related_field(self):
|
||||
parts = get_field_parts(User, 'favorite_books__title')
|
||||
|
||||
self.assertEqual(len(parts), 2)
|
||||
self.assertIsInstance(parts[0], models.ManyToManyField)
|
||||
self.assertIsInstance(parts[1], models.CharField)
|
||||
|
||||
def test_reverse_related_field(self):
|
||||
parts = get_field_parts(User, 'manager_of__users__username')
|
||||
|
||||
self.assertEqual(len(parts), 3)
|
||||
self.assertIsInstance(parts[0], ForeignObjectRel)
|
||||
self.assertIsInstance(parts[1], models.ManyToManyField)
|
||||
self.assertIsInstance(parts[2], models.CharField)
|
||||
|
||||
|
||||
class GetModelFieldTests(TestCase):
|
||||
|
||||
def test_non_existent_field(self):
|
||||
|
@ -172,3 +205,83 @@ class ResolveFieldTests(TestCase):
|
|||
exc = str(context.exception)
|
||||
self.assertIn(str(model_field), exc)
|
||||
self.assertIn('date__invalid_lookup', exc)
|
||||
|
||||
|
||||
class VerboseFieldNameTests(TestCase):
|
||||
|
||||
def test_none(self):
|
||||
verbose_name = verbose_field_name(Article, None)
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
def test_invalid_name(self):
|
||||
verbose_name = verbose_field_name(Article, 'foobar')
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
def test_field(self):
|
||||
verbose_name = verbose_field_name(Article, 'author')
|
||||
self.assertEqual(verbose_name, 'author')
|
||||
|
||||
def test_field_with_verbose_name(self):
|
||||
verbose_name = verbose_field_name(Article, 'name')
|
||||
self.assertEqual(verbose_name, 'title')
|
||||
|
||||
def test_forwards_related_field(self):
|
||||
verbose_name = verbose_field_name(Article, 'author__username')
|
||||
self.assertEqual(verbose_name, 'author username')
|
||||
|
||||
def test_backwards_related_field(self):
|
||||
verbose_name = verbose_field_name(Book, 'lovers__first_name')
|
||||
self.assertEqual(verbose_name, 'lovers first name')
|
||||
|
||||
def test_lazy_text(self):
|
||||
# sanity check
|
||||
field = User._meta.get_field('username')
|
||||
self.assertIsInstance(field.verbose_name, Promise)
|
||||
|
||||
verbose_name = verbose_field_name(User, 'username')
|
||||
self.assertEqual(verbose_name, 'username')
|
||||
|
||||
|
||||
class VerboseLookupExprTests(TestCase):
|
||||
|
||||
def test_exact(self):
|
||||
# Exact should default to empty. A verbose expression is unnecessary,
|
||||
# and this behavior works well with list syntax for `Meta.fields`.
|
||||
verbose_lookup = verbose_lookup_expr('exact')
|
||||
self.assertEqual(verbose_lookup, '')
|
||||
|
||||
def test_verbose_expression(self):
|
||||
verbose_lookup = verbose_lookup_expr('date__lt')
|
||||
self.assertEqual(verbose_lookup, 'date is less than')
|
||||
|
||||
def test_missing_keys(self):
|
||||
verbose_lookup = verbose_lookup_expr('foo__bar__lt')
|
||||
self.assertEqual(verbose_lookup, 'foo bar is less than')
|
||||
|
||||
@override_settings(FILTERS_VERBOSE_LOOKUPS={'exact': 'is equal to'})
|
||||
def test_overridden_settings(self):
|
||||
verbose_lookup = verbose_lookup_expr('exact')
|
||||
self.assertEqual(verbose_lookup, 'is equal to')
|
||||
|
||||
|
||||
class LabelForFilterTests(TestCase):
|
||||
|
||||
def test_standard_label(self):
|
||||
label = label_for_filter(Article, 'name', 'in')
|
||||
self.assertEqual(label, 'Title is in')
|
||||
|
||||
def test_related_model(self):
|
||||
label = label_for_filter(Article, 'author__first_name', 'in')
|
||||
self.assertEqual(label, 'Author first name is in')
|
||||
|
||||
def test_exclusion_label(self):
|
||||
label = label_for_filter(Article, 'name', 'in', exclude=True)
|
||||
self.assertEqual(label, 'Exclude title is in')
|
||||
|
||||
def test_related_model_exclusion(self):
|
||||
label = label_for_filter(Article, 'author__first_name', 'in', exclude=True)
|
||||
self.assertEqual(label, 'Exclude author first name is in')
|
||||
|
||||
def test_exact_lookup(self):
|
||||
label = label_for_filter(Article, 'name', 'exact')
|
||||
self.assertEqual(label, 'Title')
|
||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import unicode_literals
|
|||
from django.test import TestCase
|
||||
from django.forms import TextInput, Select
|
||||
|
||||
from django_filters.widgets import BooleanWidget
|
||||
from django_filters.widgets import BooleanWidget, QueryArrayWidget
|
||||
from django_filters.widgets import BaseCSVWidget
|
||||
from django_filters.widgets import CSVWidget
|
||||
from django_filters.widgets import RangeWidget
|
||||
|
@ -279,3 +279,74 @@ class CSVSelectTests(TestCase):
|
|||
|
||||
self.assertHTMLEqual(w.render('price', [1, 2]), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
|
||||
class QueryArrayWidgetTests(TestCase):
|
||||
|
||||
def test_widget_value_from_datadict(self):
|
||||
w = QueryArrayWidget()
|
||||
|
||||
# Values can be provided as csv string: ?foo=bar,baz
|
||||
data = {'price': None}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price': '1'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price': '1,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price': '1,,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price': '1,'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price': ','}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price': ''}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
# Values can be provided as query array: ?foo[]=bar&foo[]=baz
|
||||
|
||||
data = {'price[]': None}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price[]': ['1']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price[]': ['1', '2']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price[]': ['1', '', '2']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price[]': ['1', '']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price[]': ['', '']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price[]': []}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
|
38
tox.ini
38
tox.ini
|
@ -1,26 +1,38 @@
|
|||
[tox]
|
||||
envlist =
|
||||
{py27,py33,py34,py35}-django18-restframework{33,34},
|
||||
{py27,py34,py35}-django{19,110}-restframework34,
|
||||
{py27,py34,py35}-django-latest-restframework34
|
||||
{py27,py33,py34,py35}-django18-restframework{34,35},
|
||||
{py27,py34,py35}-django{19,110}-restframework{34,35},
|
||||
{py27,py34,py35}-djangolatest-restframeworklatest,
|
||||
warnings
|
||||
|
||||
|
||||
[testenv]
|
||||
basepython =
|
||||
py27: python2.7
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
py35: python3.5
|
||||
|
||||
|
||||
commands = ./runtests.py {posargs}
|
||||
commands = coverage run --source django_filters ./runtests.py {posargs}
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
django18: django>=1.8.0,<1.9.0
|
||||
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
|
||||
restframework33: djangorestframework>=3.3,<3.4
|
||||
djangolatest: https://github.com/django/django/archive/master.tar.gz
|
||||
restframework34: djangorestframework>=3.4,<3.5
|
||||
restframework35: djangorestframework>=3.5,<3.6
|
||||
restframeworklatest: https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
|
||||
-rrequirements/test-ci.txt
|
||||
|
||||
[testenv:py27-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:py34-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:py35-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:warnings]
|
||||
ignore_outcome = True
|
||||
commands = python -Werror ./runtests.py {posargs}
|
||||
deps =
|
||||
https://github.com/django/django/archive/master.tar.gz
|
||||
https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
|
||||
-rrequirements/test-ci.txt
|
||||
|
|
Loading…
Reference in New Issue