commit
44af395d9b
|
@ -1,5 +1,5 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.12.0
|
current_version = 0.13.0
|
||||||
commit = False
|
commit = False
|
||||||
tag = False
|
tag = False
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
||||||
|
|
14
CHANGES.rst
14
CHANGES.rst
|
@ -1,6 +1,20 @@
|
||||||
|
Version 0.13.0 (2016-03-11)
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Add support for filtering by CSV #363
|
||||||
|
|
||||||
|
* Add DateTimeFromToRangeFilter #376
|
||||||
|
|
||||||
|
* Add Chinese translation #359
|
||||||
|
|
||||||
|
* Lots of fixes.
|
||||||
|
|
||||||
|
|
||||||
Version 0.12.0 (2016-01-07)
|
Version 0.12.0 (2016-01-07)
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
* Raised minimum Django version to 1.8.x
|
||||||
|
|
||||||
* FEATURE: Add support for custom ORM lookup types #221
|
* FEATURE: Add support for custom ORM lookup types #221
|
||||||
|
|
||||||
* FEATURE: Add JavaScript friendly BooleanWidget #270
|
* FEATURE: Add JavaScript friendly BooleanWidget #270
|
||||||
|
|
|
@ -8,3 +8,4 @@ recursive-include docs *
|
||||||
recursive-include requirements *
|
recursive-include requirements *
|
||||||
recursive-include tests *
|
recursive-include tests *
|
||||||
recursive-include django_filters/locale *
|
recursive-include django_filters/locale *
|
||||||
|
prune docs/_build
|
|
@ -6,8 +6,8 @@ querysets dynamically.
|
||||||
|
|
||||||
Full documentation on `read the docs`_.
|
Full documentation on `read the docs`_.
|
||||||
|
|
||||||
.. image:: https://secure.travis-ci.org/alex/django-filter.png?branch=master
|
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
|
||||||
:target: http://travis-ci.org/alex/django-filter
|
:target: https://travis-ci.org/carltongibson/django-filter
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
|
@ -24,7 +24,7 @@ Install using pip::
|
||||||
|
|
||||||
Or clone the repo and add to your PYTHONPATH::
|
Or clone the repo and add to your PYTHONPATH::
|
||||||
|
|
||||||
git clone git@github.com:alex/django-filter.git
|
git clone git@github.com:carltongibson/django-filter.git
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
@ -46,7 +46,7 @@ And then in your view you could do::
|
||||||
|
|
||||||
def product_list(request):
|
def product_list(request):
|
||||||
filter = ProductFilter(request.GET, queryset=Product.objects.all())
|
filter = ProductFilter(request.GET, queryset=Product.objects.all())
|
||||||
return render_to_response('my_app/template.html', {'filter': filter})
|
return render(request, 'my_app/template.html', {'filter': filter})
|
||||||
|
|
||||||
Django-filters additionally supports specifying FilterSet fields using a
|
Django-filters additionally supports specifying FilterSet fields using a
|
||||||
dictionary to specify filters with lookup types::
|
dictionary to specify filters with lookup types::
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import absolute_import
|
||||||
from .filterset import FilterSet
|
from .filterset import FilterSet
|
||||||
from .filters import *
|
from .filters import *
|
||||||
|
|
||||||
__version__ = '0.12.0'
|
__version__ = '0.13.0'
|
||||||
|
|
||||||
|
|
||||||
def parse_version(version):
|
def parse_version(version):
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
import django
|
||||||
|
|
||||||
|
|
||||||
|
def remote_field(field):
|
||||||
|
"""
|
||||||
|
https://docs.djangoproject.com/en/1.9/releases/1.9/#field-rel-changes
|
||||||
|
"""
|
||||||
|
if django.VERSION >= (1, 9):
|
||||||
|
return field.remote_field
|
||||||
|
return field.rel
|
||||||
|
|
||||||
|
|
||||||
|
def remote_model(field):
|
||||||
|
if django.VERSION >= (1, 9):
|
||||||
|
return remote_field(field).model
|
||||||
|
return remote_field(field).to
|
|
@ -5,13 +5,13 @@ from datetime import datetime, time
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.dateparse import parse_datetime
|
from django.utils.dateparse import parse_datetime
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from django.utils.encoding import force_str
|
from django.utils.encoding import force_str
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .widgets import RangeWidget, LookupTypeWidget
|
from .utils import handle_timezone
|
||||||
|
from .widgets import RangeWidget, LookupTypeWidget, CSVWidget
|
||||||
|
|
||||||
|
|
||||||
class RangeField(forms.MultiValueField):
|
class RangeField(forms.MultiValueField):
|
||||||
|
@ -42,13 +42,24 @@ class DateRangeField(RangeField):
|
||||||
if data_list:
|
if data_list:
|
||||||
start_date, stop_date = data_list
|
start_date, stop_date = data_list
|
||||||
if start_date:
|
if start_date:
|
||||||
start_date = datetime.combine(start_date, time.min)
|
start_date = handle_timezone(
|
||||||
|
datetime.combine(start_date, time.min))
|
||||||
if stop_date:
|
if stop_date:
|
||||||
stop_date = datetime.combine(stop_date, time.max)
|
stop_date = handle_timezone(
|
||||||
|
datetime.combine(stop_date, time.max))
|
||||||
return slice(start_date, stop_date)
|
return slice(start_date, stop_date)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeRangeField(RangeField):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
fields = (
|
||||||
|
forms.DateTimeField(),
|
||||||
|
forms.DateTimeField())
|
||||||
|
super(DateTimeRangeField, self).__init__(fields, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TimeRangeField(RangeField):
|
class TimeRangeField(RangeField):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -58,7 +69,12 @@ class TimeRangeField(RangeField):
|
||||||
super(TimeRangeField, self).__init__(fields, *args, **kwargs)
|
super(TimeRangeField, self).__init__(fields, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
Lookup = namedtuple('Lookup', ('value', 'lookup_type'))
|
class Lookup(namedtuple('Lookup', ('value', 'lookup_type'))):
|
||||||
|
# python nature is test __len__ on tuple types for boolean check
|
||||||
|
def __len__(self):
|
||||||
|
if not self.value:
|
||||||
|
return 0
|
||||||
|
return 2
|
||||||
|
|
||||||
|
|
||||||
class LookupTypeField(forms.MultiValueField):
|
class LookupTypeField(forms.MultiValueField):
|
||||||
|
@ -91,7 +107,6 @@ class IsoDateTimeField(forms.DateTimeField):
|
||||||
"""
|
"""
|
||||||
ISO_8601 = 'iso-8601'
|
ISO_8601 = 'iso-8601'
|
||||||
input_formats = [ISO_8601]
|
input_formats = [ISO_8601]
|
||||||
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
|
|
||||||
|
|
||||||
def strptime(self, value, format):
|
def strptime(self, value, format):
|
||||||
value = force_str(value)
|
value = force_str(value)
|
||||||
|
@ -100,12 +115,39 @@ class IsoDateTimeField(forms.DateTimeField):
|
||||||
parsed = parse_datetime(value)
|
parsed = parse_datetime(value)
|
||||||
if parsed is None: # Continue with other formats if doesn't match
|
if parsed is None: # Continue with other formats if doesn't match
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
return handle_timezone(parsed)
|
||||||
# Handle timezone awareness. Copied from:
|
|
||||||
# https://github.com/tomchristie/django-rest-framework/blob/3.2.0/rest_framework/fields.py#L965-L969
|
|
||||||
if settings.USE_TZ and not timezone.is_aware(parsed):
|
|
||||||
return timezone.make_aware(parsed, self.default_timezone)
|
|
||||||
elif not settings.USE_TZ and timezone.is_aware(parsed):
|
|
||||||
return timezone.make_naive(parsed, timezone.UTC())
|
|
||||||
return parsed
|
|
||||||
return super(IsoDateTimeField, self).strptime(value, format)
|
return super(IsoDateTimeField, self).strptime(value, format)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCSVField(forms.Field):
|
||||||
|
"""
|
||||||
|
Base field for validating CSV types. Value validation is performed by
|
||||||
|
secondary base classes.
|
||||||
|
|
||||||
|
ex::
|
||||||
|
class IntegerCSVField(BaseCSVField, filters.IntegerField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
widget = CSVWidget
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return [super(BaseCSVField, self).clean(v) for v in value]
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRangeField(BaseCSVField):
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid_values': _('Range query expects two values.')
|
||||||
|
}
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
value = super(BaseRangeField, self).clean(value)
|
||||||
|
|
||||||
|
if value is not None and len(value) != 2:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
self.error_messages['invalid_values'],
|
||||||
|
code='invalid_values')
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import warnings
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.db.models.sql.constants import QUERY_TERMS
|
from django.db.models.sql.constants import QUERY_TERMS
|
||||||
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.conf import settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .fields import (
|
from .fields import (
|
||||||
RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
|
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||||
|
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -20,25 +24,34 @@ __all__ = [
|
||||||
'TypedChoiceFilter', 'MultipleChoiceFilter', 'DateFilter',
|
'TypedChoiceFilter', 'MultipleChoiceFilter', 'DateFilter',
|
||||||
'DateTimeFilter', 'IsoDateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
|
'DateTimeFilter', 'IsoDateTimeFilter', 'TimeFilter', 'ModelChoiceFilter',
|
||||||
'ModelMultipleChoiceFilter', 'NumberFilter', 'NumericRangeFilter', 'RangeFilter',
|
'ModelMultipleChoiceFilter', 'NumberFilter', 'NumericRangeFilter', 'RangeFilter',
|
||||||
'DateRangeFilter', 'DateFromToRangeFilter', 'TimeRangeFilter',
|
'DateRangeFilter', 'DateFromToRangeFilter', 'DateTimeFromToRangeFilter',
|
||||||
'AllValuesFilter', 'MethodFilter'
|
'TimeRangeFilter', 'AllValuesFilter', 'MethodFilter'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
LOOKUP_TYPES = sorted(QUERY_TERMS)
|
LOOKUP_TYPES = sorted(QUERY_TERMS)
|
||||||
|
|
||||||
|
|
||||||
|
def _lookup_type_warning():
|
||||||
|
warnings.warn('lookup_type is deprecated. Use lookup_expr instead.', DeprecationWarning, stacklevel=3)
|
||||||
|
|
||||||
|
|
||||||
class Filter(object):
|
class Filter(object):
|
||||||
creation_counter = 0
|
creation_counter = 0
|
||||||
field_class = forms.Field
|
field_class = forms.Field
|
||||||
|
|
||||||
def __init__(self, name=None, label=None, widget=None, action=None,
|
def __init__(self, name=None, label=None, widget=None, action=None,
|
||||||
lookup_type='exact', required=False, distinct=False, exclude=False, **kwargs):
|
lookup_expr='exact', required=False, distinct=False, exclude=False, **kwargs):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.label = label
|
self.label = label
|
||||||
if action:
|
if action:
|
||||||
self.filter = action
|
self.filter = action
|
||||||
self.lookup_type = lookup_type
|
|
||||||
|
self.lookup_expr = lookup_expr
|
||||||
|
if 'lookup_type' in kwargs:
|
||||||
|
_lookup_type_warning()
|
||||||
|
self.lookup_expr = kwargs.pop('lookup_type')
|
||||||
|
|
||||||
self.widget = widget
|
self.widget = widget
|
||||||
self.required = required
|
self.required = required
|
||||||
self.extra = kwargs
|
self.extra = kwargs
|
||||||
|
@ -54,14 +67,32 @@ class Filter(object):
|
||||||
"""
|
"""
|
||||||
return qs.exclude if self.exclude else qs.filter
|
return qs.exclude if self.exclude else qs.filter
|
||||||
|
|
||||||
|
def lookup_type():
|
||||||
|
def fget(self):
|
||||||
|
_lookup_type_warning()
|
||||||
|
return self.lookup_expr
|
||||||
|
|
||||||
|
def fset(self, value):
|
||||||
|
_lookup_type_warning()
|
||||||
|
self.lookup_expr = value
|
||||||
|
|
||||||
|
return locals()
|
||||||
|
lookup_type = property(**lookup_type())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def field(self):
|
def field(self):
|
||||||
if not hasattr(self, '_field'):
|
if not hasattr(self, '_field'):
|
||||||
help_text = self.extra.pop('help_text', None)
|
help_text = self.extra.pop('help_text', None)
|
||||||
if help_text is None:
|
if help_text is None:
|
||||||
help_text = _('This is an exclusion filter') if self.exclude else _('Filter')
|
if self.exclude and getattr(settings, "FILTERS_HELP_TEXT_EXCLUDE", True):
|
||||||
if (self.lookup_type is None or
|
help_text = _('This is an exclusion filter')
|
||||||
isinstance(self.lookup_type, (list, tuple))):
|
elif not self.exclude and getattr(settings, "FILTERS_HELP_TEXT_FILTER", True):
|
||||||
|
help_text = _('Filter')
|
||||||
|
else:
|
||||||
|
help_text = ''
|
||||||
|
|
||||||
|
if (self.lookup_expr is None or
|
||||||
|
isinstance(self.lookup_expr, (list, tuple))):
|
||||||
|
|
||||||
lookup = []
|
lookup = []
|
||||||
|
|
||||||
|
@ -71,10 +102,15 @@ class Filter(object):
|
||||||
else:
|
else:
|
||||||
choice = (x, x)
|
choice = (x, x)
|
||||||
|
|
||||||
if self.lookup_type is None:
|
if self.lookup_expr is None:
|
||||||
lookup.append(choice)
|
|
||||||
elif x in self.lookup_type:
|
|
||||||
lookup.append(choice)
|
lookup.append(choice)
|
||||||
|
else:
|
||||||
|
if isinstance(x, (list, tuple)) and len(x) == 2:
|
||||||
|
if x[0] in self.lookup_expr:
|
||||||
|
lookup.append(choice)
|
||||||
|
else:
|
||||||
|
if x in self.lookup_expr:
|
||||||
|
lookup.append(choice)
|
||||||
|
|
||||||
self._field = LookupTypeField(self.field_class(
|
self._field = LookupTypeField(self.field_class(
|
||||||
required=self.required, widget=self.widget, **self.extra),
|
required=self.required, widget=self.widget, **self.extra),
|
||||||
|
@ -90,7 +126,7 @@ class Filter(object):
|
||||||
lookup = six.text_type(value.lookup_type)
|
lookup = six.text_type(value.lookup_type)
|
||||||
value = value.value
|
value = value.value
|
||||||
else:
|
else:
|
||||||
lookup = self.lookup_type
|
lookup = self.lookup_expr
|
||||||
if value in ([], (), {}, None, ''):
|
if value in ([], (), {}, None, ''):
|
||||||
return qs
|
return qs
|
||||||
if self.distinct:
|
if self.distinct:
|
||||||
|
@ -229,7 +265,7 @@ class NumericRangeFilter(Filter):
|
||||||
def filter(self, qs, value):
|
def filter(self, qs, value):
|
||||||
if value:
|
if value:
|
||||||
if value.start is not None and value.stop is not None:
|
if value.start is not None and value.stop is not None:
|
||||||
lookup = '%s__%s' % (self.name, self.lookup_type)
|
lookup = '%s__%s' % (self.name, self.lookup_expr)
|
||||||
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
||||||
else:
|
else:
|
||||||
if value.start is not None:
|
if value.start is not None:
|
||||||
|
@ -305,6 +341,10 @@ class DateFromToRangeFilter(RangeFilter):
|
||||||
field_class = DateRangeField
|
field_class = DateRangeField
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeFromToRangeFilter(RangeFilter):
|
||||||
|
field_class = DateTimeRangeField
|
||||||
|
|
||||||
|
|
||||||
class TimeRangeFilter(RangeFilter):
|
class TimeRangeFilter(RangeFilter):
|
||||||
field_class = TimeRangeField
|
field_class = TimeRangeField
|
||||||
|
|
||||||
|
@ -318,6 +358,65 @@ class AllValuesFilter(ChoiceFilter):
|
||||||
return super(AllValuesFilter, self).field
|
return super(AllValuesFilter, self).field
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCSVFilter(Filter):
|
||||||
|
"""
|
||||||
|
Base class for CSV type filters, such as IN and RANGE.
|
||||||
|
"""
|
||||||
|
base_field_class = BaseCSVField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(BaseCSVFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
class ConcreteCSVField(self.base_field_class, self.field_class):
|
||||||
|
pass
|
||||||
|
ConcreteCSVField.__name__ = self._field_class_name(
|
||||||
|
self.field_class, self.lookup_expr
|
||||||
|
)
|
||||||
|
|
||||||
|
self.field_class = ConcreteCSVField
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _field_class_name(cls, field_class, lookup_expr):
|
||||||
|
"""
|
||||||
|
Generate a suitable class name for the concrete field class. This is not
|
||||||
|
completely reliable, as not all field class names are of the format
|
||||||
|
<Type>Field.
|
||||||
|
|
||||||
|
ex::
|
||||||
|
|
||||||
|
BaseCSVFilter._field_class_name(DateTimeField, 'year__in')
|
||||||
|
|
||||||
|
returns 'DateTimeYearInField'
|
||||||
|
|
||||||
|
"""
|
||||||
|
# DateTimeField => DateTime
|
||||||
|
type_name = field_class.__name__
|
||||||
|
if type_name.endswith('Field'):
|
||||||
|
type_name = type_name[:-5]
|
||||||
|
|
||||||
|
# year__in => YearIn
|
||||||
|
parts = lookup_expr.split(LOOKUP_SEP)
|
||||||
|
expression_name = ''.join(p.capitalize() for p in parts)
|
||||||
|
|
||||||
|
# DateTimeYearInField
|
||||||
|
return str('%s%sField' % (type_name, expression_name))
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInFilter(BaseCSVFilter):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault('lookup_expr', 'in')
|
||||||
|
super(BaseInFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRangeFilter(BaseCSVFilter):
|
||||||
|
base_field_class = BaseRangeField
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs.setdefault('lookup_expr', 'range')
|
||||||
|
super(BaseRangeFilter, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class MethodFilter(Filter):
|
class MethodFilter(Filter):
|
||||||
"""
|
"""
|
||||||
This filter will allow you to run a method that exists on the filterset class
|
This filter will allow you to run a method that exists on the filterset class
|
||||||
|
|
|
@ -10,15 +10,16 @@ from django.forms.forms import NON_FIELD_ERRORS
|
||||||
from django.core.validators import EMPTY_VALUES
|
from django.core.validators import EMPTY_VALUES
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.fields import FieldDoesNotExist
|
|
||||||
from django.db.models.fields.related import ForeignObjectRel
|
from django.db.models.fields.related import ForeignObjectRel
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from .filters import (Filter, CharFilter, BooleanFilter,
|
from .compat import remote_field, remote_model
|
||||||
|
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
|
||||||
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
||||||
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter)
|
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter)
|
||||||
|
from .utils import try_dbfield, get_model_field, resolve_field
|
||||||
|
|
||||||
|
|
||||||
ORDER_BY_FIELD = 'o'
|
ORDER_BY_FIELD = 'o'
|
||||||
|
@ -56,25 +57,6 @@ def get_declared_filters(bases, attrs, with_base_filters=True):
|
||||||
return OrderedDict(filters)
|
return OrderedDict(filters)
|
||||||
|
|
||||||
|
|
||||||
def get_model_field(model, f):
|
|
||||||
parts = f.split(LOOKUP_SEP)
|
|
||||||
opts = model._meta
|
|
||||||
for name in parts[:-1]:
|
|
||||||
try:
|
|
||||||
rel = opts.get_field(name)
|
|
||||||
except FieldDoesNotExist:
|
|
||||||
return None
|
|
||||||
if isinstance(rel, ForeignObjectRel):
|
|
||||||
opts = rel.related_model._meta
|
|
||||||
else:
|
|
||||||
opts = rel.rel.to._meta
|
|
||||||
try:
|
|
||||||
rel = opts.get_field(parts[-1])
|
|
||||||
except FieldDoesNotExist:
|
|
||||||
return None
|
|
||||||
return rel
|
|
||||||
|
|
||||||
|
|
||||||
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
||||||
filter_for_reverse_field=None):
|
filter_for_reverse_field=None):
|
||||||
field_dict = OrderedDict()
|
field_dict = OrderedDict()
|
||||||
|
@ -99,14 +81,17 @@ def filters_for_model(model, fields=None, exclude=None, filter_for_field=None,
|
||||||
# If fields is a dictionary, it must contain lists.
|
# If fields is a dictionary, it must contain lists.
|
||||||
elif isinstance(fields, dict):
|
elif isinstance(fields, dict):
|
||||||
# Create a filter for each lookup type.
|
# Create a filter for each lookup type.
|
||||||
for lookup_type in fields[f]:
|
for lookup_expr in fields[f]:
|
||||||
filter_ = filter_for_field(field, f, lookup_type)
|
filter_ = filter_for_field(field, f, lookup_expr)
|
||||||
|
|
||||||
if filter_:
|
if filter_:
|
||||||
filter_name = f
|
filter_name = LOOKUP_SEP.join([f, lookup_expr])
|
||||||
|
|
||||||
# Don't add "exact" to filter names
|
# Don't add "exact" to filter names
|
||||||
if lookup_type != 'exact':
|
_exact = LOOKUP_SEP + 'exact'
|
||||||
filter_name = f + LOOKUP_SEP + lookup_type
|
if filter_name.endswith(_exact):
|
||||||
|
filter_name = filter_name[:-len(_exact)]
|
||||||
|
|
||||||
field_dict[filter_name] = filter_
|
field_dict[filter_name] = filter_
|
||||||
# If fields is a list, it contains strings.
|
# If fields is a list, it contains strings.
|
||||||
else:
|
else:
|
||||||
|
@ -178,9 +163,10 @@ class FilterSetMetaclass(type):
|
||||||
else:
|
else:
|
||||||
filters = declared_filters
|
filters = declared_filters
|
||||||
|
|
||||||
if None in filters.values():
|
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 "
|
raise TypeError("Meta.fields contains a field that isn't defined "
|
||||||
"on this FilterSet")
|
"on this FilterSet: {}".format(not_defined))
|
||||||
|
|
||||||
new_class.declared_filters = declared_filters
|
new_class.declared_filters = declared_filters
|
||||||
new_class.base_filters = filters
|
new_class.base_filters = filters
|
||||||
|
@ -212,24 +198,24 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
|
||||||
models.OneToOneField: {
|
models.OneToOneField: {
|
||||||
'filter_class': ModelChoiceFilter,
|
'filter_class': ModelChoiceFilter,
|
||||||
'extra': lambda f: {
|
'extra': lambda f: {
|
||||||
'queryset': f.rel.to._default_manager.complex_filter(
|
'queryset': remote_model(f)._default_manager.complex_filter(
|
||||||
f.rel.limit_choices_to),
|
remote_field(f).limit_choices_to),
|
||||||
'to_field_name': f.rel.field_name,
|
'to_field_name': remote_field(f).field_name,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models.ForeignKey: {
|
models.ForeignKey: {
|
||||||
'filter_class': ModelChoiceFilter,
|
'filter_class': ModelChoiceFilter,
|
||||||
'extra': lambda f: {
|
'extra': lambda f: {
|
||||||
'queryset': f.rel.to._default_manager.complex_filter(
|
'queryset': remote_model(f)._default_manager.complex_filter(
|
||||||
f.rel.limit_choices_to),
|
remote_field(f).limit_choices_to),
|
||||||
'to_field_name': f.rel.field_name
|
'to_field_name': remote_field(f).field_name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models.ManyToManyField: {
|
models.ManyToManyField: {
|
||||||
'filter_class': ModelMultipleChoiceFilter,
|
'filter_class': ModelMultipleChoiceFilter,
|
||||||
'extra': lambda f: {
|
'extra': lambda f: {
|
||||||
'queryset': f.rel.to._default_manager.complex_filter(
|
'queryset': remote_model(f)._default_manager.complex_filter(
|
||||||
f.rel.limit_choices_to),
|
remote_field(f).limit_choices_to),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
models.DecimalField: {
|
models.DecimalField: {
|
||||||
|
@ -355,7 +341,9 @@ class BaseFilterSet(object):
|
||||||
except forms.ValidationError:
|
except forms.ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if ordered_value in EMPTY_VALUES and self.strict:
|
# With a None-queryset, ordering must be enforced (#84).
|
||||||
|
if (ordered_value in EMPTY_VALUES and
|
||||||
|
self.strict == STRICTNESS.RETURN_NO_RESULTS):
|
||||||
ordered_value = self.form.fields[self.order_by_field].choices[0][0]
|
ordered_value = self.form.fields[self.order_by_field].choices[0][0]
|
||||||
|
|
||||||
if ordered_value:
|
if ordered_value:
|
||||||
|
@ -430,41 +418,24 @@ class BaseFilterSet(object):
|
||||||
return [order_choice]
|
return [order_choice]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_for_field(cls, f, name, lookup_type='exact'):
|
def filter_for_field(cls, f, name, lookup_expr='exact'):
|
||||||
filter_for_field = dict(FILTER_FOR_DBFIELD_DEFAULTS)
|
f, lookup_type = resolve_field(f, lookup_expr)
|
||||||
filter_for_field.update(cls.filter_overrides)
|
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
'name': name,
|
'name': name,
|
||||||
'label': capfirst(f.verbose_name),
|
'label': capfirst(f.verbose_name),
|
||||||
'lookup_type': lookup_type
|
'lookup_expr': lookup_expr
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.choices:
|
filter_class, params = cls.filter_for_lookup(f, lookup_type)
|
||||||
default['choices'] = f.choices
|
default.update(params)
|
||||||
return ChoiceFilter(**default)
|
|
||||||
|
|
||||||
data = filter_for_field.get(f.__class__)
|
|
||||||
if data is None:
|
|
||||||
# could be a derived field, inspect parents
|
|
||||||
for class_ in f.__class__.mro():
|
|
||||||
# skip if class_ is models.Field or object
|
|
||||||
# 1st item in mro() is original class
|
|
||||||
if class_ in (f.__class__, models.Field, object):
|
|
||||||
continue
|
|
||||||
data = filter_for_field.get(class_)
|
|
||||||
if data:
|
|
||||||
break
|
|
||||||
if data is None:
|
|
||||||
return
|
|
||||||
filter_class = data.get('filter_class')
|
|
||||||
default.update(data.get('extra', lambda f: {})(f))
|
|
||||||
if filter_class is not None:
|
if filter_class is not None:
|
||||||
return filter_class(**default)
|
return filter_class(**default)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_for_reverse_field(cls, f, name):
|
def filter_for_reverse_field(cls, f, name):
|
||||||
rel = f.field.rel
|
rel = remote_field(f.field)
|
||||||
queryset = f.field.model._default_manager.all()
|
queryset = f.field.model._default_manager.all()
|
||||||
default = {
|
default = {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
@ -476,6 +447,76 @@ class BaseFilterSet(object):
|
||||||
else:
|
else:
|
||||||
return ModelChoiceFilter(**default)
|
return ModelChoiceFilter(**default)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_for_lookup(cls, f, lookup_type):
|
||||||
|
DEFAULTS = dict(FILTER_FOR_DBFIELD_DEFAULTS)
|
||||||
|
DEFAULTS.update(cls.filter_overrides)
|
||||||
|
|
||||||
|
data = try_dbfield(DEFAULTS.get, f.__class__) or {}
|
||||||
|
filter_class = data.get('filter_class')
|
||||||
|
params = data.get('extra', lambda f: {})(f)
|
||||||
|
|
||||||
|
# if there is no filter class, exit early
|
||||||
|
if not filter_class:
|
||||||
|
return None, {}
|
||||||
|
|
||||||
|
# perform lookup specific checks
|
||||||
|
if lookup_type == 'isnull':
|
||||||
|
data = try_dbfield(DEFAULTS.get, models.BooleanField)
|
||||||
|
|
||||||
|
filter_class = data.get('filter_class')
|
||||||
|
params = data.get('extra', lambda f: {})(f)
|
||||||
|
return filter_class, params
|
||||||
|
|
||||||
|
if lookup_type == 'in':
|
||||||
|
class ConcreteInFilter(BaseInFilter, filter_class):
|
||||||
|
pass
|
||||||
|
ConcreteInFilter.__name__ = cls._csv_filter_class_name(
|
||||||
|
filter_class, lookup_type
|
||||||
|
)
|
||||||
|
|
||||||
|
return ConcreteInFilter, params
|
||||||
|
|
||||||
|
if lookup_type == 'range':
|
||||||
|
class ConcreteRangeFilter(BaseRangeFilter, filter_class):
|
||||||
|
pass
|
||||||
|
ConcreteRangeFilter.__name__ = cls._csv_filter_class_name(
|
||||||
|
filter_class, lookup_type
|
||||||
|
)
|
||||||
|
|
||||||
|
return ConcreteRangeFilter, params
|
||||||
|
|
||||||
|
# Default behavior
|
||||||
|
if f.choices:
|
||||||
|
return ChoiceFilter, {'choices': f.choices}
|
||||||
|
|
||||||
|
return filter_class, params
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _csv_filter_class_name(cls, filter_class, lookup_type):
|
||||||
|
"""
|
||||||
|
Generate a suitable class name for a concrete filter class. This is not
|
||||||
|
completely reliable, as not all filter class names are of the format
|
||||||
|
<Type>Filter.
|
||||||
|
|
||||||
|
ex::
|
||||||
|
|
||||||
|
FilterSet._csv_filter_class_name(DateTimeFilter, 'in')
|
||||||
|
|
||||||
|
returns 'DateTimeInFilter'
|
||||||
|
|
||||||
|
"""
|
||||||
|
# DateTimeFilter => DateTime
|
||||||
|
type_name = filter_class.__name__
|
||||||
|
if type_name.endswith('Filter'):
|
||||||
|
type_name = type_name[:-6]
|
||||||
|
|
||||||
|
# in => In
|
||||||
|
lookup_name = lookup_type.capitalize()
|
||||||
|
|
||||||
|
# DateTimeInFilter
|
||||||
|
return str('%s%sFilter' % (type_name, lookup_name))
|
||||||
|
|
||||||
|
|
||||||
class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
|
class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# Kane Blueriver <kxxoling@gmail.com>, 2016.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2016-01-30 17:39+0800\n"
|
||||||
|
"PO-Revision-Date: 2016-01-30 17:50+0800\n"
|
||||||
|
"Last-Translator: Kane Blueriver <kxxoling@gmail.com>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||||
|
|
||||||
|
#: filters.py:62
|
||||||
|
msgid "This is an exclusion filter"
|
||||||
|
msgstr "未启用该过滤器"
|
||||||
|
|
||||||
|
#: filters.py:62
|
||||||
|
msgid "Filter"
|
||||||
|
msgstr "过滤器"
|
||||||
|
|
||||||
|
#: filters.py:264
|
||||||
|
msgid "Any date"
|
||||||
|
msgstr "任何时刻"
|
||||||
|
|
||||||
|
#: filters.py:265
|
||||||
|
msgid "Today"
|
||||||
|
msgstr "今日"
|
||||||
|
|
||||||
|
#: filters.py:270
|
||||||
|
msgid "Past 7 days"
|
||||||
|
msgstr "过去 7 日"
|
||||||
|
|
||||||
|
#: filters.py:274
|
||||||
|
msgid "This month"
|
||||||
|
msgstr "本月"
|
||||||
|
|
||||||
|
#: filters.py:278
|
||||||
|
msgid "This year"
|
||||||
|
msgstr "今年"
|
||||||
|
|
||||||
|
#: filters.py:281
|
||||||
|
msgid "Yesterday"
|
||||||
|
msgstr "昨日"
|
||||||
|
|
||||||
|
#: filterset.py:398 filterset.py:409
|
||||||
|
#, python-format
|
||||||
|
msgid "%s (descending)"
|
||||||
|
msgstr "%s(降序)"
|
||||||
|
|
||||||
|
#: filterset.py:411
|
||||||
|
msgid "Ordering"
|
||||||
|
msgstr "排序"
|
||||||
|
|
||||||
|
#: widgets.py:60
|
||||||
|
msgid "All"
|
||||||
|
msgstr "全部"
|
|
@ -0,0 +1,104 @@
|
||||||
|
from django.conf import settings
|
||||||
|
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.utils import timezone
|
||||||
|
|
||||||
|
from .compat import remote_model
|
||||||
|
|
||||||
|
|
||||||
|
def try_dbfield(fn, field_class):
|
||||||
|
"""
|
||||||
|
Try ``fn`` with the DB ``field_class`` by walking its
|
||||||
|
MRO until a result is found.
|
||||||
|
|
||||||
|
ex::
|
||||||
|
_try_dbfield(field_dict.get, models.CharField)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# walk the mro, as field_class could be a derived model field.
|
||||||
|
for cls in field_class.mro():
|
||||||
|
# skip if cls is models.Field
|
||||||
|
if cls is models.Field:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = fn(cls)
|
||||||
|
if data:
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_field(model, field_name):
|
||||||
|
"""
|
||||||
|
Get a ``model`` field, traversing relationships
|
||||||
|
in the ``field_name``.
|
||||||
|
|
||||||
|
ex::
|
||||||
|
|
||||||
|
f = get_model_field(Book, 'author__first_name')
|
||||||
|
|
||||||
|
"""
|
||||||
|
parts = field_name.split(LOOKUP_SEP)
|
||||||
|
opts = model._meta
|
||||||
|
|
||||||
|
# walk relationships
|
||||||
|
for name in parts[:-1]:
|
||||||
|
try:
|
||||||
|
rel = 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
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_field(model_field, lookup_expr):
|
||||||
|
"""
|
||||||
|
Resolves a ``lookup_expr`` into its final output field, given
|
||||||
|
the initial ``model_field``. The lookup expression should only contain
|
||||||
|
transforms and lookups, not intermediary model field parts.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This method is based on django.db.models.sql.query.Query.build_lookup
|
||||||
|
|
||||||
|
For more info on the lookup API:
|
||||||
|
https://docs.djangoproject.com/en/1.9/ref/models/lookups/
|
||||||
|
|
||||||
|
"""
|
||||||
|
query = model_field.model._default_manager.all().query
|
||||||
|
lhs = Expression(model_field)
|
||||||
|
lookups = lookup_expr.split(LOOKUP_SEP)
|
||||||
|
|
||||||
|
assert len(lookups) > 0
|
||||||
|
|
||||||
|
while lookups:
|
||||||
|
name = lookups[0]
|
||||||
|
# If there is just one part left, try first get_lookup() so
|
||||||
|
# that if the lhs supports both transform and lookup for the
|
||||||
|
# name, then lookup will be picked.
|
||||||
|
if len(lookups) == 1:
|
||||||
|
final_lookup = lhs.get_lookup(name)
|
||||||
|
if not final_lookup:
|
||||||
|
# We didn't find a lookup. We are going to interpret
|
||||||
|
# the name as transform, and do an Exact lookup against
|
||||||
|
# it.
|
||||||
|
lhs = query.try_transform(lhs, name, lookups)
|
||||||
|
final_lookup = lhs.get_lookup('exact')
|
||||||
|
return lhs.output_field, final_lookup.lookup_name
|
||||||
|
lhs = query.try_transform(lhs, name, lookups)
|
||||||
|
lookups = lookups[1:]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_timezone(value):
|
||||||
|
if settings.USE_TZ and timezone.is_naive(value):
|
||||||
|
return timezone.make_aware(value, timezone.get_default_timezone())
|
||||||
|
elif not settings.USE_TZ and timezone.is_aware(value):
|
||||||
|
return timezone.make_naive(value, timezone.UTC())
|
||||||
|
return value
|
|
@ -1,6 +1,7 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from collections import Iterable
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
try:
|
try:
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
@ -12,6 +13,7 @@ from django.db.models.fields import BLANK_CHOICE_DASH
|
||||||
from django.forms.widgets import flatatt
|
from django.forms.widgets import flatatt
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.six import string_types
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,21 +98,58 @@ class LookupTypeWidget(forms.MultiWidget):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
class BooleanWidget(forms.Widget):
|
class BooleanWidget(forms.Select):
|
||||||
"""Convert true/false values into the internal Python True/False.
|
"""Convert true/false values into the internal Python True/False.
|
||||||
This can be used for AJAX queries that pass true/false from JavaScript's
|
This can be used for AJAX queries that pass true/false from JavaScript's
|
||||||
internal types through.
|
internal types through.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, attrs=None):
|
||||||
|
choices = (('', _('Unknown')),
|
||||||
|
('true', _('Yes')),
|
||||||
|
('false', _('No')))
|
||||||
|
super(BooleanWidget, self).__init__(attrs, choices)
|
||||||
|
|
||||||
|
def render(self, name, value, attrs=None):
|
||||||
|
try:
|
||||||
|
value = {
|
||||||
|
True: 'true',
|
||||||
|
False: 'false',
|
||||||
|
'1': 'true',
|
||||||
|
'0': 'false'
|
||||||
|
}[value]
|
||||||
|
except KeyError:
|
||||||
|
value = ''
|
||||||
|
return super(BooleanWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
def value_from_datadict(self, data, files, name):
|
||||||
"""
|
value = data.get(name, None)
|
||||||
"""
|
if isinstance(value, string_types):
|
||||||
value = super(BooleanWidget, self).value_from_datadict(
|
value = value.lower()
|
||||||
data, files, name)
|
|
||||||
|
return {
|
||||||
|
'1': True,
|
||||||
|
'0': False,
|
||||||
|
'true': True,
|
||||||
|
'false': False,
|
||||||
|
True: True,
|
||||||
|
False: False,
|
||||||
|
}.get(value, None)
|
||||||
|
|
||||||
|
|
||||||
|
class CSVWidget(forms.TextInput):
|
||||||
|
def _isiterable(self, value):
|
||||||
|
return isinstance(value, Iterable) and not isinstance(value, string_types)
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
value = super(CSVWidget, self).value_from_datadict(data, files, name)
|
||||||
|
|
||||||
if value is not None:
|
if value is not None:
|
||||||
if value.lower() == 'true':
|
return value.split(',')
|
||||||
value = True
|
return None
|
||||||
elif value.lower() == 'false':
|
|
||||||
value = False
|
|
||||||
|
|
||||||
return value
|
def render(self, name, value, attrs=None):
|
||||||
|
if self._isiterable(value):
|
||||||
|
value = [force_text(self._format_value(v)) for v in value]
|
||||||
|
value = ','.join(list(value))
|
||||||
|
|
||||||
|
return super(CSVWidget, self).render(name, value, attrs)
|
||||||
|
|
10
docs/conf.py
10
docs/conf.py
|
@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.12.0'
|
version = '0.13.0'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.12.0'
|
release = '0.13.0'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
@ -183,7 +183,7 @@ latex_elements = {
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
latex_documents = [
|
latex_documents = [
|
||||||
('asd', 'django-filter.tex', u'django-filter Documentation',
|
('index', 'django-filter.tex', u'django-filter Documentation',
|
||||||
u'Alex Gaynor and others.', 'manual'),
|
u'Alex Gaynor and others.', 'manual'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -213,7 +213,7 @@ latex_documents = [
|
||||||
# One entry per manual page. List of tuples
|
# One entry per manual page. List of tuples
|
||||||
# (source start file, name, description, authors, manual section).
|
# (source start file, name, description, authors, manual section).
|
||||||
man_pages = [
|
man_pages = [
|
||||||
('asd', 'django-filter', u'django-filter Documentation',
|
('index', 'django-filter', u'django-filter Documentation',
|
||||||
[u'Alex Gaynor and others.'], 1)
|
[u'Alex Gaynor and others.'], 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -227,7 +227,7 @@ man_pages = [
|
||||||
# (source start file, target name, title, author,
|
# (source start file, target name, title, author,
|
||||||
# dir menu entry, description, category)
|
# dir menu entry, description, category)
|
||||||
texinfo_documents = [
|
texinfo_documents = [
|
||||||
('asd', 'django-filter', u'django-filter Documentation',
|
('index', 'django-filter', u'django-filter Documentation',
|
||||||
u'Alex Gaynor and others.', 'django-filter', 'One line description of project.',
|
u'Alex Gaynor and others.', 'django-filter', 'One line description of project.',
|
||||||
'Miscellaneous'),
|
'Miscellaneous'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -14,7 +14,9 @@ Contents:
|
||||||
|
|
||||||
install
|
install
|
||||||
usage
|
usage
|
||||||
|
ref/filterset
|
||||||
ref/filters
|
ref/filters
|
||||||
ref/fields
|
ref/fields
|
||||||
ref/widgets
|
ref/widgets
|
||||||
|
ref/settings
|
||||||
tests
|
tests
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
Installing django-filter
|
Installing django-filter
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
To install, simply place the ``django_filters`` directory somewhere on your
|
Install with pip:
|
||||||
``PYTHONPATH``, and then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
|
||||||
|
pip install django-filter
|
||||||
|
|
||||||
|
And then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||||
|
|
|
@ -16,5 +16,5 @@ You may set ``input_formats`` to your list of required formats as per the `DateT
|
||||||
f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats
|
f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats
|
||||||
|
|
||||||
|
|
||||||
.. _`DateTimeField Docs`:https://docs.djangoproject.com/en/1.8/ref/forms/fields/#django.forms.DateTimeField.input_formats
|
.. _`DateTimeField Docs`: https://docs.djangoproject.com/en/1.8/ref/forms/fields/#django.forms.DateTimeField.input_formats
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,124 @@ Filter Reference
|
||||||
|
|
||||||
This is a reference document with a list of the filters and their arguments.
|
This is a reference document with a list of the filters and their arguments.
|
||||||
|
|
||||||
|
.. _core-arguments:
|
||||||
|
|
||||||
|
Core Arguments
|
||||||
|
--------------
|
||||||
|
|
||||||
|
The following are the core arguments that apply to all filters.
|
||||||
|
|
||||||
|
``name``
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The name of the field this filter is supposed to filter on, if this is not
|
||||||
|
provided it automatically becomes the filter's name on the ``FilterSet``.
|
||||||
|
You can traverse "relationship paths" using Django's ``__`` syntax to filter
|
||||||
|
fields on a related model. eg, ``manufacturer__name``.
|
||||||
|
|
||||||
|
``label``
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
The label as it will apear in the HTML, analogous to a form field's label
|
||||||
|
argument.
|
||||||
|
|
||||||
|
``widget``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
The django.form Widget class which will represent the ``Filter``. In addition
|
||||||
|
to the widgets that are included with Django that you can use there are
|
||||||
|
additional ones that django-filter provides which may be useful:
|
||||||
|
|
||||||
|
* :ref:`LinkWidget <link-widget>` -- this displays the options in a manner
|
||||||
|
similar to the way the Django Admin does, as a series of links. The link
|
||||||
|
for the selected option will have ``class="selected"``.
|
||||||
|
* :ref:`BooleanWidget <boolean-widget>` -- 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.
|
||||||
|
* :ref:`CSVWidget <csv-widget>` -- this widget expects a comma separated
|
||||||
|
value and converts it into a list of string values. It is expected that
|
||||||
|
the field class handle a list of values as well as type conversion.
|
||||||
|
|
||||||
|
``action``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
An optional callable that tells the filter how to handle the queryset. It
|
||||||
|
recieves a ``QuerySet`` and the value to filter on and should return a
|
||||||
|
``Queryset`` that is filtered appropriately. `action` will default to
|
||||||
|
``filter_{value-of-name-attribute}``
|
||||||
|
|
||||||
|
``lookup_expr``
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The lookup expression that should be performed using `Django's ORM`_.
|
||||||
|
|
||||||
|
.. _`Django's ORM`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
||||||
|
|
||||||
|
A ``list`` or ``tuple`` of lookup types is also accepted, allowing the user to
|
||||||
|
select the lookup from a dropdown. The list of lookup types are filtered against
|
||||||
|
``filters.LOOKUP_TYPES``. If `lookup_expr=None` is passed, then a list of all lookup
|
||||||
|
types will be generated::
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
name = django_filters.CharFilter(lookup_expr=['exact', 'iexact'])
|
||||||
|
|
||||||
|
You can enable custom lookups by adding them to ``LOOKUP_TYPES``::
|
||||||
|
|
||||||
|
from django_filters import filters
|
||||||
|
|
||||||
|
filters.LOOKUP_TYPES = ['gt', 'gte', 'lt', 'lte', 'custom_lookup_type']
|
||||||
|
|
||||||
|
Additionally, you can provide human-friendly help text by overriding ``LOOKUP_TYPES``::
|
||||||
|
|
||||||
|
# filters.py
|
||||||
|
from django_filters import filters
|
||||||
|
|
||||||
|
filters.LOOKUP_TYPES = [
|
||||||
|
('', '---------'),
|
||||||
|
('exact', 'Is equal to'),
|
||||||
|
('not_exact', 'Is not equal to'),
|
||||||
|
('lt', 'Lesser than'),
|
||||||
|
('gt', 'Greater than'),
|
||||||
|
('gte', 'Greater than or equal to'),
|
||||||
|
('lte', 'Lesser than or equal to'),
|
||||||
|
('startswith', 'Starts with'),
|
||||||
|
('endswith', 'Ends with'),
|
||||||
|
('contains', 'Contains'),
|
||||||
|
('not_contains', 'Does not contain'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
``distinct``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A boolean value that specifies whether the Filter will use distinct on the
|
||||||
|
queryset. This option can be used to eliminate duplicate results when using filters that span related models. Defaults to ``False``.
|
||||||
|
|
||||||
|
``exclude``
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset.
|
||||||
|
Defaults to ``False``.
|
||||||
|
|
||||||
|
|
||||||
|
``**kwargs``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Any additional keyword arguments are stored as the ``extra`` parameter on the filter. They are provided to the accompanying form Field and can be used to provide arguments like ``choices``.
|
||||||
|
|
||||||
|
|
||||||
|
ModelChoiceFilter and ModelMultipleChoiceFilter arguments
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
These arguments apply specifically to ModelChoiceFiler and
|
||||||
|
ModelMultipleChoiceFilter only.
|
||||||
|
|
||||||
|
``queryset``
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to operate on which must be passed as a kwarg.
|
||||||
|
|
||||||
|
|
||||||
Filters
|
Filters
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -171,7 +289,7 @@ than a maximum where only one limit value is provided. This filter is designed t
|
||||||
the Postgres Numerical Range Fields, including `IntegerRangeField`, `BigIntegerRangeField` and `FloatRangeField`,
|
the Postgres Numerical Range Fields, including `IntegerRangeField`, `BigIntegerRangeField` and `FloatRangeField`,
|
||||||
available since Django 1.8. The default widget used is the `RangeField`.
|
available since Django 1.8. The default widget used is the `RangeField`.
|
||||||
|
|
||||||
RangeField lookup_types can be used, including `overlap`, `contains`, and `contained_by`. More lookups can be
|
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](https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields)).
|
found in the Django docs ([https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields](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
|
If the lower limit value is provided, the filter automatically defaults to `__startswith` as the lookup
|
||||||
|
@ -211,14 +329,118 @@ selections for working with date fields.
|
||||||
``DateFromToRangeFilter``
|
``DateFromToRangeFilter``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Similar to a ``RangeFilter`` except it uses dates instead of numerical values. It can be used with ``DateField`` and ``DateTimeField``.
|
Similar to a ``RangeFilter`` except it uses dates instead of numerical values. It can be used with ``DateField``. It also works with ``DateTimeField``, but takes into consideration only the date.
|
||||||
|
|
||||||
|
Example of using the ``DateField`` field::
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
date = models.DateField()
|
||||||
|
time = models.TimeField()
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
date = DateFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['date']
|
||||||
|
|
||||||
|
# Range: Comments added between 2016-01-01 and 2016-02-01
|
||||||
|
f = F({'date_0': '2016-01-01', 'date_1': '2016-02-01'})
|
||||||
|
|
||||||
|
# Min-Only: Comments added after 2016-01-01
|
||||||
|
f = F({'date_0': '2016-01-01'})
|
||||||
|
|
||||||
|
# Max-Only: Comments added before 2016-02-01
|
||||||
|
f = F({'date_1': '2016-02-01'})
|
||||||
|
|
||||||
|
Example of using the ``DateTimeField`` field::
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
published = models.DateTimeField()
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
published = DateFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ['published']
|
||||||
|
|
||||||
|
Article.objects.create(published='2016-01-01 8:00')
|
||||||
|
Article.objects.create(published='2016-01-20 10:00')
|
||||||
|
Article.objects.create(published='2016-02-10 12:00')
|
||||||
|
|
||||||
|
# Range: Articles published between 2016-01-01 and 2016-02-01
|
||||||
|
f = F({'published_0': '2016-01-01', 'published_1': '2016-02-01'})
|
||||||
|
assert len(f.qs) == 2
|
||||||
|
|
||||||
|
# Min-Only: Articles published after 2016-01-01
|
||||||
|
f = F({'published_0': '2016-01-01'})
|
||||||
|
assert len(f.qs) == 3
|
||||||
|
|
||||||
|
# Max-Only: Articles published before 2016-02-01
|
||||||
|
f = F({'published_1': '2016-02-01'})
|
||||||
|
assert len(f.qs) == 2
|
||||||
|
|
||||||
|
``DateTimeFromToRangeFilter``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Similar to a ``RangeFilter`` except it uses datetime format values instead of numerical values. It can be used with ``DateTimeField``.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
class Article(models.Model):
|
||||||
|
published = models.DateTimeField()
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
published = DateTimeFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ['published']
|
||||||
|
|
||||||
|
Article.objects.create(published='2016-01-01 8:00')
|
||||||
|
Article.objects.create(published='2016-01-01 9:30')
|
||||||
|
Article.objects.create(published='2016-01-02 8:00')
|
||||||
|
|
||||||
|
# Range: Articles published 2016-01-01 between 8:00 and 10:00
|
||||||
|
f = F({'published_0': '2016-01-01 8:00', 'published_1': '2016-01-01 10:00'})
|
||||||
|
assert len(f.qs) == 2
|
||||||
|
|
||||||
|
# Min-Only: Articles published after 2016-01-01 8:00
|
||||||
|
f = F({'published_0': '2016-01-01 8:00'})
|
||||||
|
assert len(f.qs) == 3
|
||||||
|
|
||||||
|
# Max-Only: Articles published before 2016-01-01 10:00
|
||||||
|
f = F({'published_1': '2016-01-01 10:00'})
|
||||||
|
assert len(f.qs) == 2
|
||||||
|
|
||||||
``TimeRangeFilter``
|
``TimeRangeFilter``
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Similar to a ``RangeFilter`` except it uses time format values instead of numerical values. It can be used with ``TimeField``.
|
Similar to a ``RangeFilter`` except it uses time format values instead of numerical values. It can be used with ``TimeField``.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
class Comment(models.Model):
|
||||||
|
date = models.DateField()
|
||||||
|
time = models.TimeField()
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
time = TimeRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['time']
|
||||||
|
|
||||||
|
# Range: Comments added between 8:00 and 10:00
|
||||||
|
f = F({'time_0': '8:00', 'time_1': '10:00'})
|
||||||
|
|
||||||
|
# Min-Only: Comments added after 8:00
|
||||||
|
f = F({'time_0': '8:00'})
|
||||||
|
|
||||||
|
# Max-Only: Comments added before 10:00
|
||||||
|
f = F({'time_1': '10:00'})
|
||||||
|
|
||||||
``AllValuesFilter``
|
``AllValuesFilter``
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -227,9 +449,60 @@ database. So if in the DB for the given field you have values of 5, 7, and 9
|
||||||
each of those is present as an option. This is similar to the default behavior
|
each of those is present as an option. This is similar to the default behavior
|
||||||
of the admin.
|
of the admin.
|
||||||
|
|
||||||
``MethodFilter``
|
``BaseInFilter``
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a base class used for generating IN lookup filters. Values are expected to be comma separated.
|
||||||
|
|
||||||
|
Example. ::
|
||||||
|
|
||||||
|
class NumberInFilter(BaseInFilter, NumericFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
id__in = NumberInFilter(name=id, lookup_expr='in')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
User.objects.create(username='alex')
|
||||||
|
User.objects.create(username='jacob')
|
||||||
|
User.objects.create(username='aaron')
|
||||||
|
User.objects.create(username='carl')
|
||||||
|
|
||||||
|
# In: User with IDs 1 and 3.
|
||||||
|
f = F({'id__in': '1,3'})
|
||||||
|
assert len(f.qs) == 2
|
||||||
|
|
||||||
|
``BaseRangeFilter``
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This is a base class used for generating RANGE lookup filters. The base class expects two values two, separated by a comma.
|
||||||
|
|
||||||
|
Example. ::
|
||||||
|
|
||||||
|
class NumberRangeFilter(BaseInFilter, NumericFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
id__range = NumberRangeFilter(name=id, lookup_expr='range')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
|
||||||
|
User.objects.create(username='alex')
|
||||||
|
User.objects.create(username='jacob')
|
||||||
|
User.objects.create(username='aaron')
|
||||||
|
User.objects.create(username='carl')
|
||||||
|
|
||||||
|
# Range: User with IDs between 1 and 3.
|
||||||
|
f = F({'id__range': '1,3'})
|
||||||
|
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 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
|
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}``
|
filter set class. `action` will default to ``filter_{value-of-name-attribute}``
|
||||||
|
@ -248,79 +521,3 @@ Example. ::
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
fields = ['published']
|
fields = ['published']
|
||||||
|
|
||||||
Core Arguments
|
|
||||||
--------------
|
|
||||||
|
|
||||||
These arguments apply to the filters described above.
|
|
||||||
|
|
||||||
``name``
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
The name of the field this filter is supposed to filter on, if this is not
|
|
||||||
provided it automatically becomes the filter's name on the ``FilterSet``.
|
|
||||||
|
|
||||||
``label``
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
The label as it will apear in the HTML, analogous to a form field's label
|
|
||||||
argument.
|
|
||||||
|
|
||||||
``widget``
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
The django.form Widget class which will represent the ``Filter``. In addition
|
|
||||||
to the widgets that are included with Django that you can use there are
|
|
||||||
additional ones that django-filter provides which may be useful:
|
|
||||||
|
|
||||||
* ``django_filters.widgets.LinkWidget`` -- this displays the options in a
|
|
||||||
manner similar to the way the Django Admin does, as a series of links.
|
|
||||||
The link for the selected option will have ``class="selected"``.
|
|
||||||
|
|
||||||
``action``
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
An optional callable that tells the filter how to handle the queryset. It
|
|
||||||
recieves a ``QuerySet`` and the value to filter on and should return a
|
|
||||||
``Queryset`` that is filtered appropriately. `action` will default to
|
|
||||||
``filter_{value-of-name-attribute}``
|
|
||||||
|
|
||||||
``lookup_type``
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The type of lookup that should be performed using the [Django ORM](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups "Django's ORM Lookups").
|
|
||||||
All the normal options are allowed, and should be provided as a string. You can also
|
|
||||||
provide either ``None`` or a ``list`` or a ``tuple``. If ``None`` is provided,
|
|
||||||
then the user can select the lookup type from all the ones available in the Django
|
|
||||||
ORM. If a ``list`` or ``tuple`` is provided, then the user can select from those
|
|
||||||
options.
|
|
||||||
|
|
||||||
``distinct``
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
A boolean value that specifies whether the Filter will use distinct on the
|
|
||||||
queryset. This option can be used to eliminate duplicate results when using filters that span related models. Defaults to ``False``.
|
|
||||||
|
|
||||||
``exclude``
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset.
|
|
||||||
Defaults to ``False``.
|
|
||||||
|
|
||||||
ModelChoiceFilter and ModelMultipleChoiceFilter arguments
|
|
||||||
--------------
|
|
||||||
|
|
||||||
These arguments apply specifically to ModelChoiceFiler and
|
|
||||||
ModelMultipleChoiceFilter only.
|
|
||||||
|
|
||||||
``queryset``
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to operate on which must be passed as a kwarg.
|
|
||||||
|
|
||||||
|
|
||||||
``**kwargs``
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Any extra keyword arguments will be provided to the accompanying form Field.
|
|
||||||
This can be used to provide arguments like ``choices``.
|
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
FilterSet Guide
|
||||||
|
===============
|
||||||
|
|
||||||
|
This document provides a guide on using additional FilterSet features.
|
||||||
|
|
||||||
|
Meta options
|
||||||
|
------------
|
||||||
|
|
||||||
|
- model
|
||||||
|
- fields
|
||||||
|
- exclude
|
||||||
|
- :ref:`order_by <order-by>`
|
||||||
|
- :ref:`form <form>`
|
||||||
|
- :ref:`together <together>`
|
||||||
|
|
||||||
|
|
||||||
|
.. _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`` class to the string value you would like to use.
|
||||||
|
|
||||||
|
|
||||||
|
.. _form:
|
||||||
|
|
||||||
|
Custom Forms using ``form``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The inner ``Meta`` class also takes an optional ``form`` argument. This is a
|
||||||
|
form class from which ``FilterSet.form`` will subclass. This works similar to
|
||||||
|
the ``form`` option on a ``ModelAdmin.``
|
||||||
|
|
||||||
|
|
||||||
|
.. _together:
|
||||||
|
|
||||||
|
Group fields with ``together``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
||||||
|
is a list of lists, each containing field names. For convenience can be a
|
||||||
|
single list/tuple when dealing with a single set of fields. Fields within a
|
||||||
|
field set must either be all or none present in the request for
|
||||||
|
``FilterSet.form`` to be valid::
|
||||||
|
|
||||||
|
import django_filters
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = ['price', 'release_date', 'rating']
|
||||||
|
together = ['rating', 'price']
|
||||||
|
|
||||||
|
|
||||||
|
Non-Meta options
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Note that these options do not go in the Meta class, they are specified directly
|
||||||
|
in your FilterSet class.
|
||||||
|
|
||||||
|
- filter_overrides
|
||||||
|
- order_by_field
|
||||||
|
- :ref:`strict <strict>`
|
||||||
|
|
||||||
|
|
||||||
|
.. _strict:
|
||||||
|
|
||||||
|
``strict``
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
The ``strict`` option controls whether results are returned when an invalid
|
||||||
|
value is specified by the user for any filter field. By default, ``strict`` is
|
||||||
|
set to ``STRICTNESS.RETURN_NO_RESULTS`` meaning that an empty queryset is
|
||||||
|
returned if any field contains an invalid value. You can loosen this behavior
|
||||||
|
by setting ``strict`` to ``STRICTNESS.IGNORE`` which will effectively ignore a
|
||||||
|
filter field if its value is invalid. A third option of
|
||||||
|
``STRICTNESS.RAISE_VALIDATION_ERROR`` will cause a ``ValidationError`` to be
|
||||||
|
raised if any field contains an invalid value.
|
||||||
|
|
||||||
|
Overriding ``FilterSet`` methods
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
``filter_for_lookup()``
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Prior to version 0.13.0, filter generation did not take into account the
|
||||||
|
``lookup_expr`` used. This commonly caused malformed filters to be generated
|
||||||
|
for 'isnull', 'in', and 'range' lookups (as well as transformed lookups). The
|
||||||
|
current implementation provides the following behavior:
|
||||||
|
|
||||||
|
- 'isnull' lookups return a ``BooleanFilter``
|
||||||
|
- 'in' lookups return a filter derived from the CSV-based ``BaseInFilter``.
|
||||||
|
- 'range' lookups return a filter derived from the CSV-based ``BaseRangeFilter``.
|
||||||
|
|
||||||
|
If you want to override the ``filter_class`` and ``params`` used to instantiate
|
||||||
|
filters for a model field, you can override ``filter_for_lookup()``. Ex::
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
fields = {
|
||||||
|
'release_date': ['exact', 'range'],
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def filter_for_lookup(cls, f, lookup_type):
|
||||||
|
# override date range lookups
|
||||||
|
if isinstance(f, models.DateField) and lookup_type == 'range':
|
||||||
|
return django_filters.DateRangeFiler, {}
|
||||||
|
|
||||||
|
# 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)
|
|
@ -0,0 +1,24 @@
|
||||||
|
.. _ref-settings:
|
||||||
|
|
||||||
|
==================
|
||||||
|
Available Settings
|
||||||
|
==================
|
||||||
|
|
||||||
|
Here is a list of all available settings of django-filters and their
|
||||||
|
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
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
Default: ``True``
|
||||||
|
|
||||||
|
Enable / disable the default field ``help_text`` on filters where ``exclude=True`` (:doc:`/ref/filters`). This does not affect fields which explicitly define ``help_text``.
|
||||||
|
|
||||||
|
FILTERS_HELP_TEXT_FILTER
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Default: ``True``
|
||||||
|
|
||||||
|
Enable / disable the default field ``help_text`` on filters where ``exclude=False`` (:doc:`/ref/filters`). This does not affect fields which explicitly define ``help_text``.
|
|
@ -4,13 +4,16 @@ Widget Reference
|
||||||
This is a reference document with a list of the provided widgets and their
|
This is a reference document with a list of the provided widgets and their
|
||||||
arguments.
|
arguments.
|
||||||
|
|
||||||
|
|
||||||
|
.. _link-widget:
|
||||||
|
|
||||||
``LinkWidget``
|
``LinkWidget``
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
This widget renders each option as a link, instead of an actual <input>. It has
|
This widget renders each option as a link, instead of an actual <input>. It has
|
||||||
one method that you can override for additional customizability.
|
one method that you can override for additional customizability.
|
||||||
``option_string()`` should return a string with 3 Python keyword argument
|
``option_string()`` should return a string with 3 Python keyword argument
|
||||||
placeholders::
|
placeholders:
|
||||||
|
|
||||||
1. ``attrs``: This is a string with all the attributes that will be on the
|
1. ``attrs``: This is a string with all the attributes that will be on the
|
||||||
final ``<a>`` tag.
|
final ``<a>`` tag.
|
||||||
|
@ -18,6 +21,9 @@ placeholders::
|
||||||
option on the ``<a>`` element.
|
option on the ``<a>`` element.
|
||||||
3. ``label``: This is the text to be displayed to the user.
|
3. ``label``: This is the text to be displayed to the user.
|
||||||
|
|
||||||
|
|
||||||
|
.. _boolean-widget:
|
||||||
|
|
||||||
``BooleanWidget``
|
``BooleanWidget``
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -26,3 +32,13 @@ 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``::
|
||||||
|
|
||||||
active = BooleanFilter(widget=BooleanWidget())
|
active = BooleanFilter(widget=BooleanWidget())
|
||||||
|
|
||||||
|
|
||||||
|
.. _csv-widget:
|
||||||
|
|
||||||
|
``CSVWidget``
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
This widget expects a comma separated value and converts it into a list of
|
||||||
|
string values. It is expected that the field class handle a list of values as
|
||||||
|
well as type conversion.
|
||||||
|
|
288
docs/usage.txt
288
docs/usage.txt
|
@ -28,6 +28,8 @@ price or the release_date. We create a ``FilterSet`` for this::
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
name = django_filters.CharFilter(lookup_expr='iexact')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['price', 'release_date']
|
fields = ['price', 'release_date']
|
||||||
|
@ -35,42 +37,109 @@ price or the release_date. We create a ``FilterSet`` for this::
|
||||||
|
|
||||||
As you can see this uses a very similar API to Django's ``ModelForm``. Just
|
As you can see this uses a very similar API to Django's ``ModelForm``. Just
|
||||||
like with a ``ModelForm`` we can also override filters, or add new ones using a
|
like with a ``ModelForm`` we can also override filters, or add new ones using a
|
||||||
declarative syntax::
|
declarative syntax.
|
||||||
|
|
||||||
|
Declaring filters
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The declarative syntax provides you with the most flexibility when creating
|
||||||
|
filters, however it is fairly verbose. We'll use the below example to outline
|
||||||
|
the :ref:`core filter arguments <core-arguments>` on a ``FilterSet``::
|
||||||
|
|
||||||
|
class ProductFilter(django_filters.FilterSet):
|
||||||
|
price = django_filters.NumberFilter()
|
||||||
|
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
|
||||||
|
price__lt = django_filters.NumberFilter(name='price', lookup_expr='lt')
|
||||||
|
|
||||||
|
release_year = django_filters.NumberFilter(name='release_date', lookup_expr='year')
|
||||||
|
release_year__gt = django_filters.NumberFilter(name='release_date', lookup_expr='year__gt')
|
||||||
|
release_year__lt = django_filters.NumberFilter(name='release_date', lookup_expr='year__lt')
|
||||||
|
|
||||||
|
manufacturer__name = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Product
|
||||||
|
|
||||||
|
There are two main arguments for filters:
|
||||||
|
|
||||||
|
- ``name``: The name of the model field to filter on. You can traverse
|
||||||
|
"relationship paths" using Django's ``__`` syntax to filter fields on a
|
||||||
|
related model. ex, ``manufacturer__name``.
|
||||||
|
- ``lookup_expr``: The `field lookup`_ to use when filtering. Django's ``__``
|
||||||
|
syntax can again be used in order to support lookup transforms.
|
||||||
|
ex, ``year__gte``.
|
||||||
|
|
||||||
|
.. _`field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
||||||
|
|
||||||
|
Together, the field ``name`` and ``lookup_expr`` represent a complete Django
|
||||||
|
lookup expression. A detailed explanation of lookup expressions is provided in
|
||||||
|
Django's `lookup reference`_. django-filter supports expressions containing
|
||||||
|
both transforms and a final lookup for version 1.9 of Django and above.
|
||||||
|
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
|
||||||
|
|
||||||
|
While both of these fields 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)
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
For more information on additional arguments, see :ref:`core-arguments`.
|
||||||
|
|
||||||
|
|
||||||
|
Generating filters with Meta.fields
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The FilterSet Meta class provides a ``fields`` attribute that can be used for
|
||||||
|
easily specifying multiple filters without significant code duplication. The
|
||||||
|
base syntax supports a list of multiple field names::
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
price = django_filters.NumberFilter(lookup_type='lt')
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['price', 'release_date']
|
fields = ['price', 'release_date']
|
||||||
|
|
||||||
Filters take a ``lookup_type`` argument which specifies what lookup type to
|
The above generates 'exact' lookups for both the 'price' and 'release_date'
|
||||||
use with `Django's ORM`_. So here when a user entered a price it would show all
|
fields.
|
||||||
Products with a price less than that.
|
|
||||||
|
|
||||||
.. _`Django's ORM`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
Additionally, a dictionary can be used to specify multiple lookup expressions
|
||||||
|
for each field::
|
||||||
**It's quite common to forget to set lookup type for `CharField`s/`TextField`s
|
|
||||||
and wonder why search for "foo" doesn't return result for "foobar". It's because
|
|
||||||
default lookup type is exact text, but you probably want `icontains` lookup
|
|
||||||
field.**
|
|
||||||
|
|
||||||
The FilterSet Meta class fields can additionally be set using a Dictionary to
|
|
||||||
specify multiple ``lookup_type`` filters without significant code duplication::
|
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
class ProductFilter(django_filters.FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = {'price': ['lt', 'gt'],
|
fields = {
|
||||||
'release_date': ['exact'],
|
'price': ['lt', 'gt'],
|
||||||
}
|
'release_date': ['exact', 'year__gt'],
|
||||||
|
}
|
||||||
|
|
||||||
The above would generate 'price__lt', 'price__gt' and 'release_date' filters.
|
The above would generate 'price__lt', 'price__gt', 'release_date', and
|
||||||
The filter lookup type keyword 'exact' is the default and therefore never added
|
'release_date__year__gt' filters.
|
||||||
to a filter name.
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The filter lookup type 'exact' is an implicit default and therefore never
|
||||||
|
added to a filter name. In the above example, the release date's exact
|
||||||
|
filter is 'release_date', not 'release_date__exact'.
|
||||||
|
|
||||||
Items in the ``fields`` sequence in the ``Meta`` class may include
|
Items in the ``fields`` sequence in the ``Meta`` class may include
|
||||||
"relationship paths" using Django's ``__`` syntax to filter on fields on a
|
"relationship paths" using Django's ``__`` syntax to filter on fields on a
|
||||||
|
@ -81,20 +150,9 @@ related model::
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['manufacturer__country']
|
fields = ['manufacturer__country']
|
||||||
|
|
||||||
Filters also take any arbitrary keyword arguments which get passed onto the
|
|
||||||
``django.forms.Field`` initializer. These extra keyword arguments get stored
|
|
||||||
in ``Filter.extra``, so it's possible to override the initializer of a
|
|
||||||
``FilterSet`` to add extra ones::
|
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
Overriding default filters
|
||||||
class Meta:
|
""""""""""""""""""""""""""
|
||||||
model = Product
|
|
||||||
fields = ['manufacturer']
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ProductFilter, self).__init__(*args, **kwargs)
|
|
||||||
self.filters['manufacturer'].extra.update(
|
|
||||||
{'empty_label': 'All Manufacturers'})
|
|
||||||
|
|
||||||
Like ``django.contrib.admin.ModelAdmin``, it is possible to override
|
Like ``django.contrib.admin.ModelAdmin``, it is possible to override
|
||||||
default filters for all the models fields of the same kind using
|
default filters for all the models fields of the same kind using
|
||||||
|
@ -105,18 +163,28 @@ default filters for all the models fields of the same kind using
|
||||||
models.CharField: {
|
models.CharField: {
|
||||||
'filter_class': django_filters.CharFilter,
|
'filter_class': django_filters.CharFilter,
|
||||||
'extra': lambda f: {
|
'extra': lambda f: {
|
||||||
'lookup_type': 'icontains',
|
'lookup_expr': 'icontains',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
models.BooleanField: {
|
||||||
|
'filter_class': django_filters.BooleanFilter,
|
||||||
|
'extra': lambda f: {
|
||||||
|
'widget': 'forms.CheckboxInput',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Product
|
model = Product
|
||||||
fields = ['name']
|
fields = {
|
||||||
|
'name': ['exact'],
|
||||||
|
'release_date': ['isnull'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
MethodFilter
|
Custom filtering with MethodFilter
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If you want fine control over each individual filter attribute, you can use
|
If you want fine control over each individual filter attribute, you can use
|
||||||
the ``MethodFilter`` filter.
|
the ``MethodFilter`` filter.
|
||||||
|
|
||||||
|
@ -188,7 +256,7 @@ Now we need to write a view::
|
||||||
|
|
||||||
def product_list(request):
|
def product_list(request):
|
||||||
f = ProductFilter(request.GET, queryset=Product.objects.all())
|
f = ProductFilter(request.GET, queryset=Product.objects.all())
|
||||||
return render_to_response('my_app/template.html', {'filter': f})
|
return render(request, 'my_app/template.html', {'filter': f})
|
||||||
|
|
||||||
If a queryset argument isn't provided then all the items in the default manager
|
If a queryset argument isn't provided then all the items in the default manager
|
||||||
of the model will be used.
|
of the model will be used.
|
||||||
|
@ -196,6 +264,7 @@ of the model will be used.
|
||||||
If you want to access the filtered objects in your views, for example if you
|
If you want to access the filtered objects in your views, for example if you
|
||||||
want to paginate them, you can do that. They are in f.qs
|
want to paginate them, you can do that. They are in f.qs
|
||||||
|
|
||||||
|
|
||||||
The URL conf
|
The URL conf
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -203,6 +272,7 @@ We need a URL pattern to call the view::
|
||||||
|
|
||||||
url(r'^list$', views.product_list)
|
url(r'^list$', views.product_list)
|
||||||
|
|
||||||
|
|
||||||
The template
|
The template
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
@ -224,113 +294,9 @@ And that's all there is to it! The ``form`` attribute contains a normal
|
||||||
Django form, and when we iterate over the ``FilterSet`` we get the objects in
|
Django form, and when we iterate over the ``FilterSet`` we get the objects in
|
||||||
the resulting queryset.
|
the resulting queryset.
|
||||||
|
|
||||||
Other Meta options
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Ordering using ``order_by``
|
Generic view & configuration
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
-----------------------------
|
||||||
|
|
||||||
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 or ordering using a list::
|
|
||||||
|
|
||||||
import django_filters
|
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
|
||||||
price = django_filters.NumberFilter(lookup_type='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`` class to the string value you would like to use.
|
|
||||||
|
|
||||||
Custom Forms using ``form``
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The inner ``Meta`` class also takes an optional ``form`` argument. This is a
|
|
||||||
form class from which ``FilterSet.form`` will subclass. This works similar to
|
|
||||||
the ``form`` option on a ``ModelAdmin.``
|
|
||||||
|
|
||||||
Group fields with ``together``
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
|
||||||
is a list of lists, each containing field names. For convenience can be a
|
|
||||||
single list/tuple when dealing with a single set of fields. Fields within a
|
|
||||||
field set must either be all or none present in the request for
|
|
||||||
``FilterSet.form`` to be valid::
|
|
||||||
|
|
||||||
import django_filters
|
|
||||||
|
|
||||||
class ProductFilter(django_filters.FilterSet):
|
|
||||||
class Meta:
|
|
||||||
model = Product
|
|
||||||
fields = ['price', 'release_date', 'rating']
|
|
||||||
together = ['rating', 'price']
|
|
||||||
|
|
||||||
|
|
||||||
Non-Meta options
|
|
||||||
----------------
|
|
||||||
|
|
||||||
Note that these options do not go in the Meta class, they are specified directly
|
|
||||||
in your FilterSet class.
|
|
||||||
|
|
||||||
``strict``
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``strict`` option controls whether results are returned when an invalid
|
|
||||||
value is specified by the user for any filter field. By default, ``strict`` is
|
|
||||||
set to ``STRICTNESS.RETURN_NO_RESULTS`` meaning that an empty queryset is
|
|
||||||
returned if any field contains an invalid value. You can loosen this behavior
|
|
||||||
by setting ``strict`` to ``STRICTNESS.IGNORE`` which will effectively ignore a
|
|
||||||
filter field if its value is invalid. A third option of
|
|
||||||
``STRICTNESS.RAISE_VALIDATION_ERROR`` will cause a ``ValidationError` to be
|
|
||||||
raised if any field contains an invalid value.
|
|
||||||
|
|
||||||
Overriding ``FilterSet`` methods
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
``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)
|
|
||||||
|
|
||||||
Generic View
|
|
||||||
------------
|
|
||||||
|
|
||||||
In addition to the above usage there is also a class-based generic view
|
In addition to the above usage there is also a class-based generic view
|
||||||
included in django-filter, which lives at ``django_filters.views.FilterView``.
|
included in django-filter, which lives at ``django_filters.views.FilterView``.
|
||||||
|
@ -365,33 +331,3 @@ to it as the class based view::
|
||||||
|
|
||||||
The needed template and its context variables will also be the same as the
|
The needed template and its context variables will also be the same as the
|
||||||
class-based view above.
|
class-based view above.
|
||||||
|
|
||||||
Custom lookup types
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
By default django-filter uses Django's ORM built-in field lookups. If you want to globally accept specific lookups you can do the following:
|
|
||||||
|
|
||||||
from django_filters import filters
|
|
||||||
|
|
||||||
filters.LOOKUP_TYPES = ['gt', 'gte', 'lt', 'lte', 'custom_lookup_type']
|
|
||||||
|
|
||||||
Choices help text
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
If you want the ``ChoiceField`` created from `LOOKUP_TYPES` to have human-friendly options you can do the following:
|
|
||||||
|
|
||||||
from django_filters import filters
|
|
||||||
|
|
||||||
filters.LOOKUP_TYPES = [
|
|
||||||
('', '---------'),
|
|
||||||
('exact', 'Is equal to'),
|
|
||||||
('not_exact', 'Is not equal to'),
|
|
||||||
('lt', 'Lesser than'),
|
|
||||||
('gt', 'Greater than'),
|
|
||||||
('gte', 'Greater than or equal to'),
|
|
||||||
('lte', 'Lesser than or equal to'),
|
|
||||||
('startswith', 'Starts with'),
|
|
||||||
('endswith', 'Ends with'),
|
|
||||||
('contains', 'Contains'),
|
|
||||||
('not_contains', 'Does not contain'),
|
|
||||||
]
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Sphinx
|
|
|
@ -1,4 +1,30 @@
|
||||||
bumpversion
|
alabaster==0.7.7
|
||||||
twine==1.5.0
|
argh==0.26.1
|
||||||
wheel==0.24.0
|
Babel==2.2.0
|
||||||
coverage==3.7.1
|
backports.ssl-match-hostname==3.4.0.2
|
||||||
|
bumpversion==0.5.3
|
||||||
|
certifi==2015.9.6.2
|
||||||
|
coverage==3.7.1
|
||||||
|
django-discover-runner==1.0
|
||||||
|
docutils==0.12
|
||||||
|
funcsigs==0.4
|
||||||
|
Jinja2==2.8
|
||||||
|
livereload==2.4.0
|
||||||
|
MarkupSafe==0.23
|
||||||
|
mock==1.3.0
|
||||||
|
pathtools==0.1.2
|
||||||
|
pbr==1.7.0
|
||||||
|
pkginfo==1.2.1
|
||||||
|
Pygments==2.1.3
|
||||||
|
pytz==2015.7
|
||||||
|
PyYAML==3.11
|
||||||
|
requests==2.9.1
|
||||||
|
requests-toolbelt==0.6.0
|
||||||
|
six==1.9.0
|
||||||
|
snowballstemmer==1.2.1
|
||||||
|
Sphinx==1.3.6
|
||||||
|
sphinx-autobuild==0.5.2
|
||||||
|
sphinx-rtd-theme==0.1.9
|
||||||
|
tornado==4.2.1
|
||||||
|
twine==1.6.5
|
||||||
|
watchdog==0.8.3
|
||||||
|
|
6
setup.py
6
setup.py
|
@ -6,7 +6,7 @@ f = open('README.rst')
|
||||||
readme = f.read()
|
readme = f.read()
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
version = '0.12.0'
|
version = '0.13.0'
|
||||||
|
|
||||||
if sys.argv[-1] == 'publish':
|
if sys.argv[-1] == 'publish':
|
||||||
if os.system("pip freeze | grep wheel"):
|
if os.system("pip freeze | grep wheel"):
|
||||||
|
@ -30,7 +30,9 @@ setup(
|
||||||
long_description=readme,
|
long_description=readme,
|
||||||
author='Alex Gaynor',
|
author='Alex Gaynor',
|
||||||
author_email='alex.gaynor@gmail.com',
|
author_email='alex.gaynor@gmail.com',
|
||||||
url='http://github.com/alex/django-filter/tree/master',
|
maintainer='Carlton Gibson',
|
||||||
|
maintainer_email='carlton.gibson@noumenal.es',
|
||||||
|
url='http://github.com/carltongibson/django-filter/tree/master',
|
||||||
packages=find_packages(exclude=['tests']),
|
packages=find_packages(exclude=['tests']),
|
||||||
package_data = {
|
package_data = {
|
||||||
'django_filters': [
|
'django_filters': [
|
||||||
|
|
|
@ -67,7 +67,7 @@ class AdminUser(User):
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Comment(models.Model):
|
class Comment(models.Model):
|
||||||
text = models.TextField()
|
text = models.TextField()
|
||||||
author = models.ForeignKey(User, related_name='comments')
|
author = models.ForeignKey(User, related_name='comments', on_delete=models.CASCADE)
|
||||||
|
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
time = models.TimeField()
|
time = models.TimeField()
|
||||||
|
@ -78,7 +78,7 @@ class Comment(models.Model):
|
||||||
|
|
||||||
class Article(models.Model):
|
class Article(models.Model):
|
||||||
published = models.DateTimeField()
|
published = models.DateTimeField()
|
||||||
author = models.ForeignKey(User, null=True)
|
author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.author_id:
|
if self.author_id:
|
||||||
|
@ -125,7 +125,7 @@ class Company(models.Model):
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Location(models.Model):
|
class Location(models.Model):
|
||||||
company = models.ForeignKey(Company, related_name='locations')
|
company = models.ForeignKey(Company, related_name='locations', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
zip_code = models.CharField(max_length=10)
|
zip_code = models.CharField(max_length=10)
|
||||||
open_days = models.CharField(max_length=7)
|
open_days = models.CharField(max_length=7)
|
||||||
|
@ -141,7 +141,7 @@ class Account(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(models.Model):
|
||||||
account = models.OneToOneField(Account, related_name='profile')
|
account = models.OneToOneField(Account, related_name='profile', on_delete=models.CASCADE)
|
||||||
likes_coffee = models.BooleanField(default=False)
|
likes_coffee = models.BooleanField(default=False)
|
||||||
likes_tea = models.BooleanField(default=False)
|
likes_tea = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@ -169,8 +169,8 @@ class Worker(models.Model):
|
||||||
class HiredWorker(models.Model):
|
class HiredWorker(models.Model):
|
||||||
salary = models.IntegerField()
|
salary = models.IntegerField()
|
||||||
hired_on = models.DateField()
|
hired_on = models.DateField()
|
||||||
worker = models.ForeignKey(Worker)
|
worker = models.ForeignKey(Worker, on_delete=models.CASCADE)
|
||||||
business = models.ForeignKey('Business')
|
business = models.ForeignKey('Business', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
|
||||||
class Business(models.Model):
|
class Business(models.Model):
|
||||||
|
|
|
@ -8,17 +8,33 @@ import unittest
|
||||||
import django
|
import django
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware, get_default_timezone
|
||||||
|
|
||||||
from django_filters.widgets import RangeWidget
|
from django_filters.widgets import RangeWidget
|
||||||
from django_filters.fields import (
|
from django_filters.fields import (
|
||||||
RangeField, LookupTypeField, Lookup, DateRangeField, TimeRangeField, IsoDateTimeField)
|
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||||
|
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def to_d(float_value):
|
def to_d(float_value):
|
||||||
return decimal.Decimal('%.2f' % float_value)
|
return decimal.Decimal('%.2f' % float_value)
|
||||||
|
|
||||||
|
|
||||||
|
class LookupBoolTests(TestCase):
|
||||||
|
def test_lookup_true(self):
|
||||||
|
self.assertTrue(Lookup(True, 'exact'))
|
||||||
|
self.assertTrue(Lookup(1, 'exact'))
|
||||||
|
self.assertTrue(Lookup('1', 'exact'))
|
||||||
|
self.assertTrue(Lookup(datetime.now(), 'exact'))
|
||||||
|
|
||||||
|
def test_lookup_false(self):
|
||||||
|
self.assertFalse(Lookup(False, 'exact'))
|
||||||
|
self.assertFalse(Lookup(0, 'exact'))
|
||||||
|
self.assertFalse(Lookup('', 'exact'))
|
||||||
|
self.assertFalse(Lookup(None, 'exact'))
|
||||||
|
|
||||||
|
|
||||||
class RangeFieldTests(TestCase):
|
class RangeFieldTests(TestCase):
|
||||||
|
|
||||||
def test_field(self):
|
def test_field(self):
|
||||||
|
@ -40,16 +56,32 @@ class DateRangeFieldTests(TestCase):
|
||||||
f = DateRangeField()
|
f = DateRangeField()
|
||||||
self.assertEqual(len(f.fields), 2)
|
self.assertEqual(len(f.fields), 2)
|
||||||
|
|
||||||
|
@override_settings(USE_TZ=False)
|
||||||
def test_clean(self):
|
def test_clean(self):
|
||||||
w = RangeWidget()
|
w = RangeWidget()
|
||||||
f = DateRangeField(widget=w)
|
f = DateRangeField(widget=w)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
f.clean(['2015-01-01', '2015-01-10']),
|
f.clean(['2015-01-01', '2015-01-10']),
|
||||||
slice(datetime(2015, 1, 1, 0, 0 , 0),
|
slice(datetime(2015, 1, 1, 0, 0, 0),
|
||||||
datetime(2015, 1, 10, 23, 59, 59, 999999)))
|
datetime(2015, 1, 10, 23, 59, 59, 999999)))
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeRangeFieldTests(TestCase):
|
||||||
|
|
||||||
|
def test_field(self):
|
||||||
|
f = DateTimeRangeField()
|
||||||
|
self.assertEqual(len(f.fields), 2)
|
||||||
|
|
||||||
|
@override_settings(USE_TZ=False)
|
||||||
|
def test_clean(self):
|
||||||
|
w = RangeWidget()
|
||||||
|
f = DateTimeRangeField(widget=w)
|
||||||
|
self.assertEqual(
|
||||||
|
f.clean(['2015-01-01 10:30', '2015-01-10 8:45']),
|
||||||
|
slice(datetime(2015, 1, 1, 10, 30, 0),
|
||||||
|
datetime(2015, 1, 10, 8, 45, 0)))
|
||||||
|
|
||||||
|
|
||||||
class TimeRangeFieldTests(TestCase):
|
class TimeRangeFieldTests(TestCase):
|
||||||
|
|
||||||
def test_field(self):
|
def test_field(self):
|
||||||
|
@ -138,7 +170,7 @@ class IsoDateTimeFieldTests(TestCase):
|
||||||
def test_datetime_timezone_awareness(self):
|
def test_datetime_timezone_awareness(self):
|
||||||
# parsed datetimes should obey USE_TZ
|
# parsed datetimes should obey USE_TZ
|
||||||
f = IsoDateTimeField()
|
f = IsoDateTimeField()
|
||||||
r = make_aware(self.reference_dt, f.default_timezone)
|
r = make_aware(self.reference_dt, get_default_timezone())
|
||||||
|
|
||||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||||
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
||||||
|
@ -154,10 +186,6 @@ class IsoDateTimeFieldTests(TestCase):
|
||||||
f = IsoDateTimeField()
|
f = IsoDateTimeField()
|
||||||
r = self.reference_dt.replace()
|
r = self.reference_dt.replace()
|
||||||
|
|
||||||
# It's necessary to override this here, since the field class is parsed
|
|
||||||
# when USE_TZ = True.
|
|
||||||
f.default_timezone = None
|
|
||||||
|
|
||||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||||
self.assertTrue(d.tzinfo is None)
|
self.assertTrue(d.tzinfo is None)
|
||||||
self.assertEqual(d, r - timedelta(hours=1))
|
self.assertEqual(d, r - timedelta(hours=1))
|
||||||
|
@ -165,3 +193,50 @@ class IsoDateTimeFieldTests(TestCase):
|
||||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||||
self.assertTrue(d.tzinfo is None)
|
self.assertTrue(d.tzinfo is None)
|
||||||
self.assertEqual(d, r)
|
self.assertEqual(d, r)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCSVFieldTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class DecimalCSVField(BaseCSVField, forms.DecimalField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.field = DecimalCSVField()
|
||||||
|
|
||||||
|
def test_clean(self):
|
||||||
|
self.assertEqual(self.field.clean(None), None)
|
||||||
|
self.assertEqual(self.field.clean(''), [])
|
||||||
|
self.assertEqual(self.field.clean(['1']), [1])
|
||||||
|
self.assertEqual(self.field.clean(['1', '2']), [1, 2])
|
||||||
|
self.assertEqual(self.field.clean(['1', '2', '3']), [1, 2, 3])
|
||||||
|
|
||||||
|
def test_validation_error(self):
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean([''])
|
||||||
|
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean(['a', 'b', 'c'])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRangeFieldTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class DecimalRangeField(BaseRangeField, forms.DecimalField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.field = DecimalRangeField()
|
||||||
|
|
||||||
|
def test_clean(self):
|
||||||
|
self.assertEqual(self.field.clean(None), None)
|
||||||
|
self.assertEqual(self.field.clean(['1', '2']), [1, 2])
|
||||||
|
|
||||||
|
def test_validation_error(self):
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean('')
|
||||||
|
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean([''])
|
||||||
|
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean(['1'])
|
||||||
|
|
||||||
|
with self.assertRaises(forms.ValidationError):
|
||||||
|
self.field.clean(['1', '2', '3'])
|
||||||
|
|
|
@ -5,22 +5,27 @@ import datetime
|
||||||
import mock
|
import mock
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from django.test import TestCase
|
import django
|
||||||
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from django_filters.filters import AllValuesFilter
|
from django_filters.filters import AllValuesFilter
|
||||||
|
from django_filters.filters import BaseInFilter
|
||||||
from django_filters.filters import CharFilter
|
from django_filters.filters import CharFilter
|
||||||
from django_filters.filters import ChoiceFilter
|
from django_filters.filters import ChoiceFilter
|
||||||
from django_filters.filters import DateRangeFilter
|
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 DateTimeFilter
|
||||||
from django_filters.filters import MethodFilter
|
from django_filters.filters import MethodFilter
|
||||||
from django_filters.filters import MultipleChoiceFilter
|
from django_filters.filters import MultipleChoiceFilter
|
||||||
from django_filters.filters import ModelMultipleChoiceFilter
|
from django_filters.filters import ModelMultipleChoiceFilter
|
||||||
from django_filters.filters import NumberFilter
|
from django_filters.filters import NumberFilter
|
||||||
from django_filters.filters import RangeFilter
|
from django_filters.filters import RangeFilter
|
||||||
|
from django_filters.filters import TimeRangeFilter
|
||||||
# from django_filters.widgets import LinkWidget
|
# from django_filters.widgets import LinkWidget
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
@ -446,9 +451,9 @@ class NumberFilterTests(TestCase):
|
||||||
f = F({'price': 10}, queryset=Book.objects.all())
|
f = F({'price': 10}, queryset=Book.objects.all())
|
||||||
self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title)
|
self.assertQuerysetEqual(f.qs, ['Ender\'s Game'], lambda o: o.title)
|
||||||
|
|
||||||
def test_filtering_with_single_lookup_type(self):
|
def test_filtering_with_single_lookup_expr(self):
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
price = NumberFilter(lookup_type='lt')
|
price = NumberFilter(lookup_expr='lt')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
|
@ -458,7 +463,7 @@ class NumberFilterTests(TestCase):
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
f.qs, ['Ender\'s Game', 'Rainbow Six'], lambda o: o.title)
|
f.qs, ['Ender\'s Game', 'Rainbow Six'], lambda o: o.title)
|
||||||
|
|
||||||
def test_filtering_with_single_lookup_type_dictionary(self):
|
def test_filtering_with_single_lookup_expr_dictionary(self):
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
|
@ -468,9 +473,9 @@ class NumberFilterTests(TestCase):
|
||||||
self.assertQuerysetEqual(
|
self.assertQuerysetEqual(
|
||||||
f.qs, ['Ender\'s Game', 'Rainbow Six'], lambda o: o.title)
|
f.qs, ['Ender\'s Game', 'Rainbow Six'], lambda o: o.title)
|
||||||
|
|
||||||
def test_filtering_with_multiple_lookup_types(self):
|
def test_filtering_with_multiple_lookup_exprs(self):
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
price = NumberFilter(lookup_type=['lt', 'gt'])
|
price = NumberFilter(lookup_expr=['lt', 'gt'])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
|
@ -487,7 +492,7 @@ class NumberFilterTests(TestCase):
|
||||||
lambda o: o.title, ordered=False)
|
lambda o: o.title, ordered=False)
|
||||||
|
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
price = NumberFilter(lookup_type=['lt', 'gt', 'exact'])
|
price = NumberFilter(lookup_expr=['lt', 'gt', 'exact'])
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
|
@ -619,6 +624,102 @@ class DateRangeFilterTests(TestCase):
|
||||||
# different things to both user and server.
|
# different things to both user and server.
|
||||||
|
|
||||||
|
|
||||||
|
class DateFromToRangeFilterTests(TestCase):
|
||||||
|
|
||||||
|
def test_filtering(self):
|
||||||
|
adam = User.objects.create(username='adam')
|
||||||
|
kwargs = {'text': 'test', 'author': adam, 'time': '10:00'}
|
||||||
|
Comment.objects.create(date=datetime.date(2016, 1, 1), **kwargs)
|
||||||
|
Comment.objects.create(date=datetime.date(2016, 1, 2), **kwargs)
|
||||||
|
Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs)
|
||||||
|
Comment.objects.create(date=datetime.date(2016, 1, 3), **kwargs)
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
published = DateFromToRangeFilter(name='date')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['date']
|
||||||
|
|
||||||
|
results = F(data={
|
||||||
|
'published_0': '2016-01-02',
|
||||||
|
'published_1': '2016-01-03'})
|
||||||
|
self.assertEqual(len(results.qs), 3)
|
||||||
|
|
||||||
|
def test_filtering_ignores_time(self):
|
||||||
|
tz = timezone.get_current_timezone()
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz))
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
published = DateFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ['published']
|
||||||
|
|
||||||
|
results = F(data={
|
||||||
|
'published_0': '2016-01-02',
|
||||||
|
'published_1': '2016-01-03'})
|
||||||
|
self.assertEqual(len(results.qs), 3)
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeFromToRangeFilterTests(TestCase):
|
||||||
|
|
||||||
|
def test_filtering(self):
|
||||||
|
tz = timezone.get_current_timezone()
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 1, 10, 0, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 2, 12, 45, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 3, 18, 15, tzinfo=tz))
|
||||||
|
Article.objects.create(
|
||||||
|
published=datetime.datetime(2016, 1, 3, 19, 30, tzinfo=tz))
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
published = DateTimeFromToRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = ['published']
|
||||||
|
|
||||||
|
results = F(data={
|
||||||
|
'published_0': '2016-01-02 10:00',
|
||||||
|
'published_1': '2016-01-03 19:00'})
|
||||||
|
self.assertEqual(len(results.qs), 2)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRangeFilterTests(TestCase):
|
||||||
|
|
||||||
|
def test_filtering(self):
|
||||||
|
adam = User.objects.create(username='adam')
|
||||||
|
kwargs = {
|
||||||
|
'text': 'test', 'author': adam, 'date': datetime.date.today()}
|
||||||
|
Comment.objects.create(time='7:30', **kwargs)
|
||||||
|
Comment.objects.create(time='8:00', **kwargs)
|
||||||
|
Comment.objects.create(time='9:30', **kwargs)
|
||||||
|
Comment.objects.create(time='11:00', **kwargs)
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
time = TimeRangeFilter()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Comment
|
||||||
|
fields = ['time']
|
||||||
|
|
||||||
|
results = F(data={
|
||||||
|
'time_0': '8:00',
|
||||||
|
'time_1': '10:00'})
|
||||||
|
self.assertEqual(len(results.qs), 2)
|
||||||
|
|
||||||
|
|
||||||
class AllValuesFilterTests(TestCase):
|
class AllValuesFilterTests(TestCase):
|
||||||
|
|
||||||
def test_filtering(self):
|
def test_filtering(self):
|
||||||
|
@ -1240,6 +1341,189 @@ class NonSymmetricalSelfReferentialRelationshipTests(TestCase):
|
||||||
self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk)
|
self.assertQuerysetEqual(f.qs, [2], lambda o: o.pk)
|
||||||
|
|
||||||
|
|
||||||
|
# use naive datetimes, as pytz is required to perform
|
||||||
|
# date lookups when timezones are involved.
|
||||||
|
@override_settings(USE_TZ=False)
|
||||||
|
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||||
|
class TransformedQueryExpressionFilterTests(TestCase):
|
||||||
|
|
||||||
|
def test_filtering(self):
|
||||||
|
now_dt = datetime.datetime.now()
|
||||||
|
after_5pm = now_dt.replace(hour=18)
|
||||||
|
before_5pm = now_dt.replace(hour=16)
|
||||||
|
|
||||||
|
u = User.objects.create(username='alex')
|
||||||
|
a = Article.objects.create(author=u, published=after_5pm)
|
||||||
|
Article.objects.create(author=u, published=before_5pm)
|
||||||
|
|
||||||
|
class F(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = {'published': ['hour__gte']}
|
||||||
|
|
||||||
|
qs = Article.objects.all()
|
||||||
|
f = F({'published__hour__gte': 17}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 1)
|
||||||
|
self.assertQuerysetEqual(f.qs, [a.pk], lambda o: o.pk)
|
||||||
|
|
||||||
|
|
||||||
|
# use naive datetimes, as pytz is required to perform
|
||||||
|
# date lookups when timezones are involved.
|
||||||
|
@override_settings(USE_TZ=False)
|
||||||
|
class CSVFilterTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
u1 = User.objects.create(username='alex', status=1)
|
||||||
|
u2 = User.objects.create(username='jacob', status=2)
|
||||||
|
User.objects.create(username='aaron', status=2)
|
||||||
|
User.objects.create(username='carl', status=0)
|
||||||
|
|
||||||
|
now_dt = datetime.datetime.now()
|
||||||
|
after_5pm = now_dt.replace(hour=18)
|
||||||
|
before_5pm = now_dt.replace(hour=16)
|
||||||
|
|
||||||
|
Article.objects.create(author=u1, published=after_5pm)
|
||||||
|
Article.objects.create(author=u2, published=after_5pm)
|
||||||
|
Article.objects.create(author=u1, published=before_5pm)
|
||||||
|
Article.objects.create(author=u2, published=before_5pm)
|
||||||
|
|
||||||
|
class UserFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = {
|
||||||
|
'username': ['in'],
|
||||||
|
'status': ['in'],
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArticleFilter(FilterSet):
|
||||||
|
class Meta:
|
||||||
|
model = Article
|
||||||
|
fields = {
|
||||||
|
'author': ['in'],
|
||||||
|
'published': ['in'],
|
||||||
|
}
|
||||||
|
|
||||||
|
self.user_filter = UserFilter
|
||||||
|
self.article_filter = ArticleFilter
|
||||||
|
|
||||||
|
self.after_5pm = after_5pm.strftime('%Y-%m-%d %H:%M:%S.%f')
|
||||||
|
self.before_5pm = before_5pm.strftime('%Y-%m-%d %H:%M:%S.%f')
|
||||||
|
|
||||||
|
def test_numeric_filtering(self):
|
||||||
|
F = self.user_filter
|
||||||
|
|
||||||
|
qs = User.objects.all()
|
||||||
|
f = F(queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'status__in': ''}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 0)
|
||||||
|
self.assertEqual(f.count(), 0)
|
||||||
|
|
||||||
|
f = F({'status__in': '0'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 1)
|
||||||
|
self.assertEqual(f.count(), 1)
|
||||||
|
|
||||||
|
f = F({'status__in': '0,2'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 3)
|
||||||
|
self.assertEqual(f.count(), 3)
|
||||||
|
|
||||||
|
f = F({'status__in': '0,,1'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
f = F({'status__in': '2'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
def test_string_filtering(self):
|
||||||
|
F = self.user_filter
|
||||||
|
|
||||||
|
qs = User.objects.all()
|
||||||
|
f = F(queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'username__in': ''}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 0)
|
||||||
|
self.assertEqual(f.count(), 0)
|
||||||
|
|
||||||
|
f = F({'username__in': 'alex'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 1)
|
||||||
|
self.assertEqual(f.count(), 1)
|
||||||
|
|
||||||
|
f = F({'username__in': 'alex,aaron'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
f = F({'username__in': 'alex,,aaron'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
f = F({'username__in': 'alex,'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 1)
|
||||||
|
self.assertEqual(f.count(), 1)
|
||||||
|
|
||||||
|
def test_datetime_filtering(self):
|
||||||
|
F = self.article_filter
|
||||||
|
after = self.after_5pm
|
||||||
|
before = self.before_5pm
|
||||||
|
|
||||||
|
qs = Article.objects.all()
|
||||||
|
f = F(queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'published__in': ''}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 0)
|
||||||
|
self.assertEqual(f.count(), 0)
|
||||||
|
|
||||||
|
f = F({'published__in': '%s' % (after, )}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
f = F({'published__in': '%s,%s' % (after, before, )}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'published__in': '%s,,%s' % (after, before, )}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'published__in': '%s,' % (after, )}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
def test_related_filtering(self):
|
||||||
|
F = self.article_filter
|
||||||
|
|
||||||
|
qs = Article.objects.all()
|
||||||
|
f = F(queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'author__in': ''}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 0)
|
||||||
|
self.assertEqual(f.count(), 0)
|
||||||
|
|
||||||
|
f = F({'author__in': '1'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
f = F({'author__in': '1,2'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'author__in': '1,,2'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 4)
|
||||||
|
self.assertEqual(f.count(), 4)
|
||||||
|
|
||||||
|
f = F({'author__in': '1,'}, queryset=qs)
|
||||||
|
self.assertEqual(len(f.qs), 2)
|
||||||
|
self.assertEqual(f.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class MiscFilterSetTests(TestCase):
|
class MiscFilterSetTests(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from datetime import date, time, timedelta
|
from datetime import date, time, timedelta, datetime
|
||||||
import mock
|
import mock
|
||||||
|
import warnings
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import django
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
from django_filters import filters
|
from django_filters import filters
|
||||||
from django_filters.fields import (
|
from django_filters.fields import (
|
||||||
Lookup,
|
Lookup,
|
||||||
RangeField,
|
RangeField,
|
||||||
DateRangeField,
|
DateRangeField,
|
||||||
|
DateTimeRangeField,
|
||||||
TimeRangeField,
|
TimeRangeField,
|
||||||
LookupTypeField)
|
LookupTypeField,
|
||||||
|
BaseCSVField)
|
||||||
from django_filters.filters import (
|
from django_filters.filters import (
|
||||||
Filter,
|
Filter,
|
||||||
CharFilter,
|
CharFilter,
|
||||||
|
@ -32,8 +34,12 @@ from django_filters.filters import (
|
||||||
RangeFilter,
|
RangeFilter,
|
||||||
DateRangeFilter,
|
DateRangeFilter,
|
||||||
DateFromToRangeFilter,
|
DateFromToRangeFilter,
|
||||||
|
DateTimeFromToRangeFilter,
|
||||||
TimeRangeFilter,
|
TimeRangeFilter,
|
||||||
AllValuesFilter,
|
AllValuesFilter,
|
||||||
|
BaseCSVFilter,
|
||||||
|
BaseInFilter,
|
||||||
|
BaseRangeFilter,
|
||||||
UUIDFilter,
|
UUIDFilter,
|
||||||
LOOKUP_TYPES)
|
LOOKUP_TYPES)
|
||||||
|
|
||||||
|
@ -44,7 +50,7 @@ class FilterTests(TestCase):
|
||||||
|
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
f = Filter()
|
f = Filter()
|
||||||
self.assertEqual(f.lookup_type, 'exact')
|
self.assertEqual(f.lookup_expr, 'exact')
|
||||||
self.assertEqual(f.exclude, False)
|
self.assertEqual(f.exclude, False)
|
||||||
|
|
||||||
def test_creation_order(self):
|
def test_creation_order(self):
|
||||||
|
@ -64,26 +70,40 @@ class FilterTests(TestCase):
|
||||||
self.assertIsInstance(field, forms.Field)
|
self.assertIsInstance(field, forms.Field)
|
||||||
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
||||||
|
|
||||||
def test_field_with_single_lookup_type(self):
|
@override_settings(FILTERS_HELP_TEXT_FILTER=False)
|
||||||
f = Filter(lookup_type='iexact')
|
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')
|
||||||
field = f.field
|
field = f.field
|
||||||
self.assertIsInstance(field, forms.Field)
|
self.assertIsInstance(field, forms.Field)
|
||||||
|
|
||||||
def test_field_with_none_lookup_type(self):
|
def test_field_with_none_lookup_expr(self):
|
||||||
f = Filter(lookup_type=None)
|
f = Filter(lookup_expr=None)
|
||||||
field = f.field
|
field = f.field
|
||||||
self.assertIsInstance(field, LookupTypeField)
|
self.assertIsInstance(field, LookupTypeField)
|
||||||
choice_field = field.fields[1]
|
choice_field = field.fields[1]
|
||||||
self.assertEqual(len(choice_field.choices), len(LOOKUP_TYPES))
|
self.assertEqual(len(choice_field.choices), len(LOOKUP_TYPES))
|
||||||
|
|
||||||
def test_field_with_lookup_type_and_exlusion(self):
|
def test_field_with_lookup_expr_and_exlusion(self):
|
||||||
f = Filter(lookup_type=None, exclude=True)
|
f = Filter(lookup_expr=None, exclude=True)
|
||||||
field = f.field
|
field = f.field
|
||||||
self.assertIsInstance(field, LookupTypeField)
|
self.assertIsInstance(field, LookupTypeField)
|
||||||
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
self.assertEqual(field.help_text, 'This is an exclusion filter')
|
||||||
|
|
||||||
def test_field_with_list_lookup_type(self):
|
def test_field_with_list_lookup_expr(self):
|
||||||
f = Filter(lookup_type=('istartswith', 'iendswith'))
|
f = Filter(lookup_expr=('istartswith', 'iendswith'))
|
||||||
field = f.field
|
field = f.field
|
||||||
self.assertIsInstance(field, LookupTypeField)
|
self.assertIsInstance(field, LookupTypeField)
|
||||||
choice_field = field.fields[1]
|
choice_field = field.fields[1]
|
||||||
|
@ -152,22 +172,22 @@ class FilterTests(TestCase):
|
||||||
|
|
||||||
def test_filtering_with_list_value(self):
|
def test_filtering_with_list_value(self):
|
||||||
qs = mock.Mock(spec=['filter'])
|
qs = mock.Mock(spec=['filter'])
|
||||||
f = Filter(name='somefield', lookup_type=['some_lookup_type'])
|
f = Filter(name='somefield', lookup_expr=['some_lookup_expr'])
|
||||||
result = f.filter(qs, Lookup('value', 'some_lookup_type'))
|
result = f.filter(qs, Lookup('value', 'some_lookup_expr'))
|
||||||
qs.filter.assert_called_once_with(somefield__some_lookup_type='value')
|
qs.filter.assert_called_once_with(somefield__some_lookup_expr='value')
|
||||||
self.assertNotEqual(qs, result)
|
self.assertNotEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_skipped_with_list_value_with_blank(self):
|
def test_filtering_skipped_with_list_value_with_blank(self):
|
||||||
qs = mock.Mock()
|
qs = mock.Mock()
|
||||||
f = Filter(name='somefield', lookup_type=['some_lookup_type'])
|
f = Filter(name='somefield', lookup_expr=['some_lookup_expr'])
|
||||||
result = f.filter(qs, Lookup('', 'some_lookup_type'))
|
result = f.filter(qs, Lookup('', 'some_lookup_expr'))
|
||||||
self.assertListEqual(qs.method_calls, [])
|
self.assertListEqual(qs.method_calls, [])
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_skipped_with_list_value_with_blank_lookup(self):
|
def test_filtering_skipped_with_list_value_with_blank_lookup(self):
|
||||||
return # Now field is required to provide valid lookup_type if it provides any
|
return # Now field is required to provide valid lookup_expr if it provides any
|
||||||
qs = mock.Mock(spec=['filter'])
|
qs = mock.Mock(spec=['filter'])
|
||||||
f = Filter(name='somefield', lookup_type=None)
|
f = Filter(name='somefield', lookup_expr=None)
|
||||||
result = f.filter(qs, Lookup('value', ''))
|
result = f.filter(qs, Lookup('value', ''))
|
||||||
qs.filter.assert_called_once_with(somefield__exact='value')
|
qs.filter.assert_called_once_with(somefield__exact='value')
|
||||||
self.assertNotEqual(qs, result)
|
self.assertNotEqual(qs, result)
|
||||||
|
@ -187,6 +207,43 @@ class FilterTests(TestCase):
|
||||||
result = qs.distinct.assert_called_once_with()
|
result = qs.distinct.assert_called_once_with()
|
||||||
self.assertNotEqual(qs, result)
|
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):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(CustomFilterWithBooleanCheckTests, self).setUp()
|
||||||
|
|
||||||
|
class CustomTestFilter(Filter):
|
||||||
|
def filter(self_, qs, value):
|
||||||
|
if not value:
|
||||||
|
return qs
|
||||||
|
return super(CustomTestFilter, self_).filter(qs, value)
|
||||||
|
|
||||||
|
self.test_filter_class = CustomTestFilter
|
||||||
|
|
||||||
|
def test_lookup_false(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = self.test_filter_class(name='somefield')
|
||||||
|
result = f.filter(qs, Lookup('', 'exact'))
|
||||||
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
|
def test_lookup_true(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = self.test_filter_class(name='somefield')
|
||||||
|
result = f.filter(qs, Lookup('somesearch', 'exact'))
|
||||||
|
qs.filter.assert_called_once_with(somefield__exact='somesearch')
|
||||||
|
self.assertNotEqual(qs, result)
|
||||||
|
|
||||||
|
|
||||||
class CharFilterTests(TestCase):
|
class CharFilterTests(TestCase):
|
||||||
|
|
||||||
|
@ -240,9 +297,9 @@ class BooleanFilterTests(TestCase):
|
||||||
self.assertListEqual(qs.method_calls, [])
|
self.assertListEqual(qs.method_calls, [])
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_lookup_type(self):
|
def test_filtering_lookup_expr(self):
|
||||||
qs = mock.Mock(spec=['filter'])
|
qs = mock.Mock(spec=['filter'])
|
||||||
f = BooleanFilter(name='somefield', lookup_type='isnull')
|
f = BooleanFilter(name='somefield', lookup_expr='isnull')
|
||||||
result = f.filter(qs, True)
|
result = f.filter(qs, True)
|
||||||
qs.filter.assert_called_once_with(somefield__isnull=True)
|
qs.filter.assert_called_once_with(somefield__isnull=True)
|
||||||
self.assertNotEqual(qs, result)
|
self.assertNotEqual(qs, result)
|
||||||
|
@ -503,10 +560,10 @@ class NumericRangeFilterTests(TestCase):
|
||||||
result = f.filter(qs, None)
|
result = f.filter(qs, None)
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_field_with_lookup_type(self):
|
def test_field_with_lookup_expr(self):
|
||||||
qs = mock.Mock()
|
qs = mock.Mock()
|
||||||
value = mock.Mock(start=20, stop=30)
|
value = mock.Mock(start=20, stop=30)
|
||||||
f = NumericRangeFilter(lookup_type=('overlap'))
|
f = NumericRangeFilter(lookup_expr=('overlap'))
|
||||||
f.filter(qs, value)
|
f.filter(qs, value)
|
||||||
qs.filter.assert_called_once_with(None__overlap=(20, 30))
|
qs.filter.assert_called_once_with(None__overlap=(20, 30))
|
||||||
|
|
||||||
|
@ -567,10 +624,10 @@ class RangeFilterTests(TestCase):
|
||||||
result = f.filter(qs, None)
|
result = f.filter(qs, None)
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_ignores_lookup_type(self):
|
def test_filtering_ignores_lookup_expr(self):
|
||||||
qs = mock.Mock()
|
qs = mock.Mock()
|
||||||
value = mock.Mock(start=20, stop=30)
|
value = mock.Mock(start=20, stop=30)
|
||||||
f = RangeFilter(lookup_type='gte')
|
f = RangeFilter(lookup_expr='gte')
|
||||||
f.filter(qs, value)
|
f.filter(qs, value)
|
||||||
qs.filter.assert_called_once_with(None__range=(20, 30))
|
qs.filter.assert_called_once_with(None__range=(20, 30))
|
||||||
|
|
||||||
|
@ -700,15 +757,61 @@ class DateFromToRangeFilterTests(TestCase):
|
||||||
result = f.filter(qs, None)
|
result = f.filter(qs, None)
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_ignores_lookup_type(self):
|
def test_filtering_ignores_lookup_expr(self):
|
||||||
qs = mock.Mock()
|
qs = mock.Mock()
|
||||||
value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6))
|
value = mock.Mock(start=date(2015, 4, 7), stop=date(2015, 9, 6))
|
||||||
f = DateFromToRangeFilter(lookup_type='gte')
|
f = DateFromToRangeFilter(lookup_expr='gte')
|
||||||
f.filter(qs, value)
|
f.filter(qs, value)
|
||||||
qs.filter.assert_called_once_with(
|
qs.filter.assert_called_once_with(
|
||||||
None__range=(date(2015, 4, 7), date(2015, 9, 6)))
|
None__range=(date(2015, 4, 7), date(2015, 9, 6)))
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeFromToRangeFilterTests(TestCase):
|
||||||
|
|
||||||
|
def test_default_field(self):
|
||||||
|
f = DateTimeFromToRangeFilter()
|
||||||
|
field = f.field
|
||||||
|
self.assertIsInstance(field, DateTimeRangeField)
|
||||||
|
|
||||||
|
def test_filtering_range(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
value = mock.Mock(
|
||||||
|
start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45))
|
||||||
|
f = DateTimeFromToRangeFilter()
|
||||||
|
f.filter(qs, value)
|
||||||
|
qs.filter.assert_called_once_with(
|
||||||
|
None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)))
|
||||||
|
|
||||||
|
def test_filtering_start(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
value = mock.Mock(start=datetime(2015, 4, 7, 8, 30), stop=None)
|
||||||
|
f = DateTimeFromToRangeFilter()
|
||||||
|
f.filter(qs, value)
|
||||||
|
qs.filter.assert_called_once_with(None__gte=datetime(2015, 4, 7, 8, 30))
|
||||||
|
|
||||||
|
def test_filtering_stop(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
value = mock.Mock(start=None, stop=datetime(2015, 9, 6, 11, 45))
|
||||||
|
f = DateTimeFromToRangeFilter()
|
||||||
|
f.filter(qs, value)
|
||||||
|
qs.filter.assert_called_once_with(None__lte=datetime(2015, 9, 6, 11, 45))
|
||||||
|
|
||||||
|
def test_filtering_skipped_with_none_value(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = DateTimeFromToRangeFilter()
|
||||||
|
result = f.filter(qs, None)
|
||||||
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
|
def test_filtering_ignores_lookup_type(self):
|
||||||
|
qs = mock.Mock()
|
||||||
|
value = mock.Mock(
|
||||||
|
start=datetime(2015, 4, 7, 8, 30), stop=datetime(2015, 9, 6, 11, 45))
|
||||||
|
f = DateTimeFromToRangeFilter(lookup_type='gte')
|
||||||
|
f.filter(qs, value)
|
||||||
|
qs.filter.assert_called_once_with(
|
||||||
|
None__range=(datetime(2015, 4, 7, 8, 30), datetime(2015, 9, 6, 11, 45)))
|
||||||
|
|
||||||
|
|
||||||
class TimeRangeFilterTests(TestCase):
|
class TimeRangeFilterTests(TestCase):
|
||||||
|
|
||||||
def test_default_field(self):
|
def test_default_field(self):
|
||||||
|
@ -744,10 +847,10 @@ class TimeRangeFilterTests(TestCase):
|
||||||
result = f.filter(qs, None)
|
result = f.filter(qs, None)
|
||||||
self.assertEqual(qs, result)
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
def test_filtering_ignores_lookup_type(self):
|
def test_filtering_ignores_lookup_expr(self):
|
||||||
qs = mock.Mock()
|
qs = mock.Mock()
|
||||||
value = mock.Mock(start=time(10, 15), stop=time(12, 30))
|
value = mock.Mock(start=time(10, 15), stop=time(12, 30))
|
||||||
f = TimeRangeFilter(lookup_type='gte')
|
f = TimeRangeFilter(lookup_expr='gte')
|
||||||
f.filter(qs, value)
|
f.filter(qs, value)
|
||||||
qs.filter.assert_called_once_with(
|
qs.filter.assert_called_once_with(
|
||||||
None__range=(time(10, 15), time(12, 30)))
|
None__range=(time(10, 15), time(12, 30)))
|
||||||
|
@ -772,7 +875,7 @@ class AllValuesFilterTests(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class LookupTypesTests(TestCase):
|
class LookupTypesTests(TestCase):
|
||||||
def test_custom_lookup_types(self):
|
def test_custom_lookup_exprs(self):
|
||||||
filters.LOOKUP_TYPES = [
|
filters.LOOKUP_TYPES = [
|
||||||
('', '---------'),
|
('', '---------'),
|
||||||
('exact', 'Is equal to'),
|
('exact', 'Is equal to'),
|
||||||
|
@ -787,12 +890,97 @@ class LookupTypesTests(TestCase):
|
||||||
('not_contains', 'Does not contain'),
|
('not_contains', 'Does not contain'),
|
||||||
]
|
]
|
||||||
|
|
||||||
f = Filter(lookup_type=None)
|
f = Filter(lookup_expr=None)
|
||||||
field = f.field
|
field = f.field
|
||||||
choice_field = field.fields[1]
|
choice_field = field.fields[1]
|
||||||
choices = choice_field.choices
|
all_choices = choice_field.choices
|
||||||
|
|
||||||
self.assertIsInstance(field, LookupTypeField)
|
self.assertIsInstance(field, LookupTypeField)
|
||||||
self.assertEqual(choices, filters.LOOKUP_TYPES)
|
self.assertEqual(all_choices, filters.LOOKUP_TYPES)
|
||||||
self.assertEqual(choices[1][0], 'exact')
|
self.assertEqual(all_choices[1][0], 'exact')
|
||||||
self.assertEqual(choices[1][1], 'Is equal to')
|
self.assertEqual(all_choices[1][1], 'Is equal to')
|
||||||
|
|
||||||
|
custom_f = Filter(lookup_expr=('endswith', 'not_contains'))
|
||||||
|
custom_field = custom_f.field
|
||||||
|
custom_choice_field = custom_field.fields[1]
|
||||||
|
my_custom_choices = custom_choice_field.choices
|
||||||
|
|
||||||
|
available_lookup_exprs = [
|
||||||
|
('endswith', 'Ends with'),
|
||||||
|
('not_contains', 'Does not contain'),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertIsInstance(custom_field, LookupTypeField)
|
||||||
|
self.assertEqual(my_custom_choices, available_lookup_exprs)
|
||||||
|
self.assertEqual(my_custom_choices[0][0], 'endswith')
|
||||||
|
self.assertEqual(my_custom_choices[0][1], 'Ends with')
|
||||||
|
self.assertEqual(my_custom_choices[1][0], 'not_contains')
|
||||||
|
self.assertEqual(my_custom_choices[1][1], 'Does not contain')
|
||||||
|
|
||||||
|
|
||||||
|
class CSVFilterTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
class NumberInFilter(BaseCSVFilter, NumberFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class DateTimeYearInFilter(BaseCSVFilter, DateTimeFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.number_in = NumberInFilter(lookup_expr='in')
|
||||||
|
self.datetimeyear_in = DateTimeYearInFilter(lookup_expr='year__in')
|
||||||
|
|
||||||
|
def test_default_field(self):
|
||||||
|
f = BaseCSVFilter()
|
||||||
|
field = f.field
|
||||||
|
self.assertIsInstance(field, forms.Field)
|
||||||
|
|
||||||
|
def test_concrete_field(self):
|
||||||
|
field = self.number_in.field
|
||||||
|
self.assertIsInstance(field, forms.DecimalField)
|
||||||
|
self.assertIsInstance(field, BaseCSVField)
|
||||||
|
self.assertEqual(field.__class__.__name__, 'DecimalInField')
|
||||||
|
|
||||||
|
field = self.datetimeyear_in.field
|
||||||
|
self.assertIsInstance(field, forms.DateTimeField)
|
||||||
|
self.assertIsInstance(field, BaseCSVField)
|
||||||
|
self.assertEqual(field.__class__.__name__, 'DateTimeYearInField')
|
||||||
|
|
||||||
|
def test_filtering(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = self.number_in
|
||||||
|
f.filter(qs, [1, 2])
|
||||||
|
qs.filter.assert_called_once_with(None__in=[1, 2])
|
||||||
|
|
||||||
|
def test_filtering_skipped_with_none_value(self):
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = self.number_in
|
||||||
|
result = f.filter(qs, None)
|
||||||
|
self.assertEqual(qs, result)
|
||||||
|
|
||||||
|
def test_field_with_lookup_expr(self):
|
||||||
|
qs = mock.Mock()
|
||||||
|
f = self.datetimeyear_in
|
||||||
|
f.filter(qs, [1, 2])
|
||||||
|
qs.filter.assert_called_once_with(None__year__in=[1, 2])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInFilterTests(TestCase):
|
||||||
|
def test_filtering(self):
|
||||||
|
class NumberInFilter(BaseInFilter, NumberFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = NumberInFilter()
|
||||||
|
f.filter(qs, [1, 2])
|
||||||
|
qs.filter.assert_called_once_with(None__in=[1, 2])
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRangeFilterTests(TestCase):
|
||||||
|
def test_filtering(self):
|
||||||
|
class NumberInFilter(BaseRangeFilter, NumberFilter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
qs = mock.Mock(spec=['filter'])
|
||||||
|
f = NumberInFilter()
|
||||||
|
f.filter(qs, [1, 2])
|
||||||
|
qs.filter.assert_called_once_with(None__range=[1, 2])
|
||||||
|
|
|
@ -4,21 +4,28 @@ import mock
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import django
|
import django
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
||||||
from django_filters.filterset import get_model_field
|
from django_filters.filterset import STRICTNESS
|
||||||
|
from django_filters.filters import BooleanFilter
|
||||||
from django_filters.filters import CharFilter
|
from django_filters.filters import CharFilter
|
||||||
from django_filters.filters import NumberFilter
|
from django_filters.filters import NumberFilter
|
||||||
from django_filters.filters import ChoiceFilter
|
from django_filters.filters import ChoiceFilter
|
||||||
from django_filters.filters import ModelChoiceFilter
|
from django_filters.filters import ModelChoiceFilter
|
||||||
from django_filters.filters import ModelMultipleChoiceFilter
|
from django_filters.filters import ModelMultipleChoiceFilter
|
||||||
from django_filters.filters import UUIDFilter
|
from django_filters.filters import UUIDFilter
|
||||||
|
from django_filters.filters import BaseInFilter
|
||||||
|
from django_filters.filters import BaseRangeFilter
|
||||||
|
|
||||||
|
from django_filters.widgets import BooleanWidget
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
from .models import AdminUser
|
from .models import AdminUser
|
||||||
|
from .models import Article
|
||||||
from .models import Book
|
from .models import Book
|
||||||
from .models import Profile
|
from .models import Profile
|
||||||
from .models import Comment
|
from .models import Comment
|
||||||
|
@ -48,14 +55,6 @@ class HelperMethodsTests(TestCase):
|
||||||
def test_get_declared_filters(self):
|
def test_get_declared_filters(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_get_model_field_none(self):
|
|
||||||
result = get_model_field(User, 'unknown__name')
|
|
||||||
self.assertIsNone(result)
|
|
||||||
|
|
||||||
def test_get_model_field(self):
|
|
||||||
result = get_model_field(Business, 'hiredworker__worker')
|
|
||||||
self.assertEqual(result, HiredWorker._meta.get_field('worker'))
|
|
||||||
|
|
||||||
@unittest.skip('todo')
|
@unittest.skip('todo')
|
||||||
def test_filters_for_model(self):
|
def test_filters_for_model(self):
|
||||||
pass
|
pass
|
||||||
|
@ -180,11 +179,57 @@ class FilterSetFilterForFieldTests(TestCase):
|
||||||
self.assertIsNotNone(result.extra['queryset'])
|
self.assertIsNotNone(result.extra['queryset'])
|
||||||
self.assertEqual(result.extra['queryset'].model, Worker)
|
self.assertEqual(result.extra['queryset'].model, Worker)
|
||||||
|
|
||||||
|
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||||
|
def test_transformed_lookup_expr(self):
|
||||||
|
f = Comment._meta.get_field('date')
|
||||||
|
result = FilterSet.filter_for_field(f, 'date', 'year__gte')
|
||||||
|
self.assertIsInstance(result, NumberFilter)
|
||||||
|
self.assertEqual(result.name, 'date')
|
||||||
|
|
||||||
@unittest.skip('todo')
|
@unittest.skip('todo')
|
||||||
def test_filter_overrides(self):
|
def test_filter_overrides(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FilterSetFilterForLookupTests(TestCase):
|
||||||
|
|
||||||
|
def test_filter_for_ISNULL_lookup(self):
|
||||||
|
f = Article._meta.get_field('author')
|
||||||
|
result, params = FilterSet.filter_for_lookup(f, 'isnull')
|
||||||
|
self.assertEqual(result, BooleanFilter)
|
||||||
|
self.assertDictEqual(params, {})
|
||||||
|
|
||||||
|
def test_filter_for_IN_lookup(self):
|
||||||
|
f = Article._meta.get_field('author')
|
||||||
|
result, params = FilterSet.filter_for_lookup(f, 'in')
|
||||||
|
self.assertTrue(issubclass(result, ModelChoiceFilter))
|
||||||
|
self.assertTrue(issubclass(result, BaseInFilter))
|
||||||
|
self.assertEqual(params['to_field_name'], 'id')
|
||||||
|
|
||||||
|
def test_filter_for_RANGE_lookup(self):
|
||||||
|
f = Article._meta.get_field('author')
|
||||||
|
result, params = FilterSet.filter_for_lookup(f, 'range')
|
||||||
|
self.assertTrue(issubclass(result, ModelChoiceFilter))
|
||||||
|
self.assertTrue(issubclass(result, BaseRangeFilter))
|
||||||
|
self.assertEqual(params['to_field_name'], 'id')
|
||||||
|
|
||||||
|
def test_isnull_with_filter_overrides(self):
|
||||||
|
class OFilterSet(FilterSet):
|
||||||
|
filter_overrides = {
|
||||||
|
models.BooleanField: {
|
||||||
|
'filter_class': BooleanFilter,
|
||||||
|
'extra': lambda f: {
|
||||||
|
'widget': BooleanWidget,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
f = Article._meta.get_field('author')
|
||||||
|
result, params = OFilterSet.filter_for_lookup(f, 'isnull')
|
||||||
|
self.assertEqual(result, BooleanFilter)
|
||||||
|
self.assertEqual(params['widget'], BooleanWidget)
|
||||||
|
|
||||||
|
|
||||||
class FilterSetFilterForReverseFieldTests(TestCase):
|
class FilterSetFilterForReverseFieldTests(TestCase):
|
||||||
|
|
||||||
def test_reverse_o2o_relationship(self):
|
def test_reverse_o2o_relationship(self):
|
||||||
|
@ -327,13 +372,15 @@ class FilterSetClassCreationTests(TestCase):
|
||||||
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
|
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
|
||||||
|
|
||||||
def test_meta_fields_containing_unknown(self):
|
def test_meta_fields_containing_unknown(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError) as excinfo:
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
username = CharFilter()
|
username = CharFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Book
|
model = Book
|
||||||
fields = ('username', 'price', 'other')
|
fields = ('username', 'price', 'other')
|
||||||
|
self.assertEqual(excinfo.exception.args, (
|
||||||
|
"Meta.fields contains a field that isn't defined on this FilterSet: other",))
|
||||||
|
|
||||||
def test_meta_fields_dictionary_containing_unknown(self):
|
def test_meta_fields_dictionary_containing_unknown(self):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
|
@ -537,7 +584,21 @@ class FilterSetOrderingTests(TestCase):
|
||||||
|
|
||||||
def test_ordering_on_unknown_value_results_in_default_ordering_without_strict(self):
|
def test_ordering_on_unknown_value_results_in_default_ordering_without_strict(self):
|
||||||
class F(FilterSet):
|
class F(FilterSet):
|
||||||
strict = False
|
strict = STRICTNESS.IGNORE
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ['username', 'status']
|
||||||
|
order_by = ['status']
|
||||||
|
|
||||||
|
self.assertFalse(F.strict)
|
||||||
|
f = F({'o': 'username'}, queryset=self.qs)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
|
||||||
|
|
||||||
|
def test_ordering_on_unknown_value_results_in_default_ordering_with_strict_raise(self):
|
||||||
|
class F(FilterSet):
|
||||||
|
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -545,6 +606,14 @@ class FilterSetOrderingTests(TestCase):
|
||||||
order_by = ['status']
|
order_by = ['status']
|
||||||
|
|
||||||
f = F({'o': 'username'}, queryset=self.qs)
|
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(
|
self.assertQuerysetEqual(
|
||||||
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
|
f.qs, ['alex', 'jacob', 'aaron', 'carl'], lambda o: o.username)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.core.exceptions import FieldError
|
||||||
|
|
||||||
|
from django_filters.utils import get_model_field, resolve_field
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
from .models import Article
|
||||||
|
from .models import Book
|
||||||
|
from .models import HiredWorker
|
||||||
|
from .models import Business
|
||||||
|
|
||||||
|
|
||||||
|
class GetModelFieldTests(TestCase):
|
||||||
|
|
||||||
|
def test_non_existent_field(self):
|
||||||
|
result = get_model_field(User, 'unknown__name')
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
def test_related_field(self):
|
||||||
|
result = get_model_field(Business, 'hiredworker__worker')
|
||||||
|
self.assertEqual(result, HiredWorker._meta.get_field('worker'))
|
||||||
|
|
||||||
|
|
||||||
|
class ResolveFieldTests(TestCase):
|
||||||
|
|
||||||
|
def test_resolve_plain_lookups(self):
|
||||||
|
"""
|
||||||
|
Check that the standard query terms can be correctly resolved.
|
||||||
|
eg, an 'EXACT' lookup on a user's username
|
||||||
|
"""
|
||||||
|
model_field = User._meta.get_field('username')
|
||||||
|
lookups = model_field.class_lookups.keys()
|
||||||
|
|
||||||
|
# This is simple - the final ouput of an untransformed field is itself.
|
||||||
|
# The lookups are the default lookups registered to the class.
|
||||||
|
for term in lookups:
|
||||||
|
field, lookup = resolve_field(model_field, term)
|
||||||
|
self.assertIsInstance(field, models.CharField)
|
||||||
|
self.assertEqual(lookup, term)
|
||||||
|
|
||||||
|
def test_resolve_forward_related_lookups(self):
|
||||||
|
"""
|
||||||
|
Check that lookups can be resolved for related fields
|
||||||
|
in the forwards direction.
|
||||||
|
"""
|
||||||
|
lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ]
|
||||||
|
|
||||||
|
# ForeignKey
|
||||||
|
model_field = Article._meta.get_field('author')
|
||||||
|
for term in lookups:
|
||||||
|
field, lookup = resolve_field(model_field, term)
|
||||||
|
self.assertIsInstance(field, models.ForeignKey)
|
||||||
|
self.assertEqual(lookup, term)
|
||||||
|
|
||||||
|
# ManyToManyField
|
||||||
|
model_field = User._meta.get_field('favorite_books')
|
||||||
|
for term in lookups:
|
||||||
|
field, lookup = resolve_field(model_field, term)
|
||||||
|
self.assertIsInstance(field, models.ManyToManyField)
|
||||||
|
self.assertEqual(lookup, term)
|
||||||
|
|
||||||
|
@unittest.skipIf(django.VERSION < (1, 9), "version does not reverse lookups")
|
||||||
|
def test_resolve_reverse_related_lookups(self):
|
||||||
|
"""
|
||||||
|
Check that lookups can be resolved for related fields
|
||||||
|
in the reverse direction.
|
||||||
|
"""
|
||||||
|
lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ]
|
||||||
|
|
||||||
|
# ManyToOneRel
|
||||||
|
model_field = User._meta.get_field('article')
|
||||||
|
for term in lookups:
|
||||||
|
field, lookup = resolve_field(model_field, term)
|
||||||
|
self.assertIsInstance(field, models.ManyToOneRel)
|
||||||
|
self.assertEqual(lookup, term)
|
||||||
|
|
||||||
|
# ManyToManyRel
|
||||||
|
model_field = Book._meta.get_field('lovers')
|
||||||
|
for term in lookups:
|
||||||
|
field, lookup = resolve_field(model_field, term)
|
||||||
|
self.assertIsInstance(field, models.ManyToManyRel)
|
||||||
|
self.assertEqual(lookup, term)
|
||||||
|
|
||||||
|
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||||
|
def test_resolve_transformed_lookups(self):
|
||||||
|
"""
|
||||||
|
Check that chained field transforms are correctly resolved.
|
||||||
|
eg, a 'date__year__gte' lookup on an article's 'published' timestamp.
|
||||||
|
"""
|
||||||
|
# Use a DateTimeField, so we can check multiple transforms.
|
||||||
|
# eg, date__year__gte
|
||||||
|
model_field = Article._meta.get_field('published')
|
||||||
|
|
||||||
|
standard_lookups = [
|
||||||
|
'exact',
|
||||||
|
'iexact',
|
||||||
|
'gte',
|
||||||
|
'gt',
|
||||||
|
'lte',
|
||||||
|
'lt',
|
||||||
|
]
|
||||||
|
|
||||||
|
date_lookups = [
|
||||||
|
'year',
|
||||||
|
'month',
|
||||||
|
'day',
|
||||||
|
'week_day',
|
||||||
|
]
|
||||||
|
|
||||||
|
datetime_lookups = date_lookups + [
|
||||||
|
'hour',
|
||||||
|
'minute',
|
||||||
|
'second',
|
||||||
|
]
|
||||||
|
|
||||||
|
# ex: 'date__gt'
|
||||||
|
for lookup in standard_lookups:
|
||||||
|
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', lookup]))
|
||||||
|
self.assertIsInstance(field, models.DateField)
|
||||||
|
self.assertEqual(resolved_lookup, lookup)
|
||||||
|
|
||||||
|
# ex: 'year__iexact'
|
||||||
|
for part in datetime_lookups:
|
||||||
|
for lookup in standard_lookups:
|
||||||
|
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join([part, lookup]))
|
||||||
|
self.assertIsInstance(field, models.IntegerField)
|
||||||
|
self.assertEqual(resolved_lookup, lookup)
|
||||||
|
|
||||||
|
# ex: 'date__year__lte'
|
||||||
|
for part in date_lookups:
|
||||||
|
for lookup in standard_lookups:
|
||||||
|
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', part, lookup]))
|
||||||
|
self.assertIsInstance(field, models.IntegerField)
|
||||||
|
self.assertEqual(resolved_lookup, lookup)
|
||||||
|
|
||||||
|
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||||
|
def test_resolve_implicit_exact_lookup(self):
|
||||||
|
# Use a DateTimeField, so we can check multiple transforms.
|
||||||
|
# eg, date__year__gte
|
||||||
|
model_field = Article._meta.get_field('published')
|
||||||
|
|
||||||
|
field, lookup = resolve_field(model_field, 'date')
|
||||||
|
self.assertIsInstance(field, models.DateField)
|
||||||
|
self.assertEqual(lookup, 'exact')
|
||||||
|
|
||||||
|
field, lookup = resolve_field(model_field, 'date__year')
|
||||||
|
self.assertIsInstance(field, models.IntegerField)
|
||||||
|
self.assertEqual(lookup, 'exact')
|
||||||
|
|
||||||
|
def test_invalid_lookup_expression(self):
|
||||||
|
model_field = Article._meta.get_field('published')
|
||||||
|
|
||||||
|
with self.assertRaises(FieldError):
|
||||||
|
field, lookup = resolve_field(model_field, 'invalid_lookup')
|
||||||
|
|
||||||
|
def test_invalid_transformed_lookup_expression(self):
|
||||||
|
model_field = Article._meta.get_field('published')
|
||||||
|
|
||||||
|
with self.assertRaises(FieldError):
|
||||||
|
field, lookup = resolve_field(model_field, 'date__invalid_lookup')
|
|
@ -5,6 +5,7 @@ from django.test import TestCase
|
||||||
from django.forms import TextInput, Select
|
from django.forms import TextInput, Select
|
||||||
|
|
||||||
from django_filters.widgets import BooleanWidget
|
from django_filters.widgets import BooleanWidget
|
||||||
|
from django_filters.widgets import CSVWidget
|
||||||
from django_filters.widgets import RangeWidget
|
from django_filters.widgets import RangeWidget
|
||||||
from django_filters.widgets import LinkWidget
|
from django_filters.widgets import LinkWidget
|
||||||
from django_filters.widgets import LookupTypeWidget
|
from django_filters.widgets import LookupTypeWidget
|
||||||
|
@ -148,6 +149,15 @@ class RangeWidgetTests(TestCase):
|
||||||
class BooleanWidgetTests(TestCase):
|
class BooleanWidgetTests(TestCase):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
def test_widget_render(self):
|
||||||
|
w = BooleanWidget()
|
||||||
|
self.assertHTMLEqual(w.render('price', ''), """
|
||||||
|
<select name="price">
|
||||||
|
<option selected="selected" value="">Unknown</option>
|
||||||
|
<option value="true">Yes</option>
|
||||||
|
<option value="false">No</option>
|
||||||
|
</select>""")
|
||||||
|
|
||||||
def test_widget_value_from_datadict(self):
|
def test_widget_value_from_datadict(self):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
@ -163,3 +173,48 @@ class BooleanWidgetTests(TestCase):
|
||||||
|
|
||||||
result = w.value_from_datadict({}, {}, 'active')
|
result = w.value_from_datadict({}, {}, 'active')
|
||||||
self.assertEqual(result, None)
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
|
||||||
|
class CSVWidgetTests(TestCase):
|
||||||
|
def test_widget(self):
|
||||||
|
w = CSVWidget()
|
||||||
|
self.assertHTMLEqual(w.render('price', None), """
|
||||||
|
<input type="text" name="price" />""")
|
||||||
|
|
||||||
|
self.assertHTMLEqual(w.render('price', ''), """
|
||||||
|
<input type="text" name="price" />""")
|
||||||
|
|
||||||
|
self.assertHTMLEqual(w.render('price', '1,2'), """
|
||||||
|
<input type="text" name="price" value="1,2" />""")
|
||||||
|
|
||||||
|
self.assertHTMLEqual(w.render('price', ['1', '2']), """
|
||||||
|
<input type="text" name="price" value="1,2" />""")
|
||||||
|
|
||||||
|
self.assertHTMLEqual(w.render('price', [1, 2]), """
|
||||||
|
<input type="text" name="price" value="1,2" />""")
|
||||||
|
|
||||||
|
def test_widget_value_from_datadict(self):
|
||||||
|
w = CSVWidget()
|
||||||
|
|
||||||
|
data = {'price': None}
|
||||||
|
result = w.value_from_datadict(data, {}, 'price')
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
||||||
|
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(result, ['1', '2'])
|
||||||
|
|
||||||
|
data = {'price': '1,,2'}
|
||||||
|
result = w.value_from_datadict(data, {}, 'price')
|
||||||
|
self.assertEqual(result, ['1', '', '2'])
|
||||||
|
|
||||||
|
data = {'price': ''}
|
||||||
|
result = w.value_from_datadict(data, {}, 'price')
|
||||||
|
self.assertEqual(result, [''])
|
||||||
|
|
||||||
|
result = w.value_from_datadict({}, {}, 'price')
|
||||||
|
self.assertEqual(result, None)
|
||||||
|
|
2
tox.ini
2
tox.ini
|
@ -20,6 +20,6 @@ setenv =
|
||||||
deps =
|
deps =
|
||||||
django17: django>=1.7.0,<1.8.0
|
django17: django>=1.7.0,<1.8.0
|
||||||
django18: django>=1.8.0,<1.9.0
|
django18: django>=1.8.0,<1.9.0
|
||||||
django19: https://www.djangoproject.com/download/1.9a1/tarball/
|
django19: django>=1.9.0,<1.10.0
|
||||||
django-latest: https://github.com/django/django/archive/master.tar.gz
|
django-latest: https://github.com/django/django/archive/master.tar.gz
|
||||||
-rrequirements/test.txt
|
-rrequirements/test.txt
|
||||||
|
|
Loading…
Reference in New Issue