Import django-filter_1.1.0.orig.tar.gz

[dgit import orig django-filter_1.1.0.orig.tar.gz]
This commit is contained in:
Antonio Terceiro 2018-01-10 13:50:46 +01:00
commit 50215cbe9c
91 changed files with 15431 additions and 0 deletions

22
AUTHORS Normal file
View File

@ -0,0 +1,22 @@
Authors
=======
Thanks to the following people for contributing to django-filter.
Ben Firshman
Alex Gaynor
Jannis Leidel
Martin Mahner
Brian Rosner
Adam Vandenberg
Florian Apolloner
Andrew Ball
Tino de Bruijn
Maximillian Dornseif
Marc Fargas
Vladimir Sidorenko
Tom Christie
Remco Wendt
Axel Haustant
Brad Erickson
Diogo Laginha

310
CHANGES.rst Normal file
View File

@ -0,0 +1,310 @@
Version 1.1 (2017-10-19)
------------------------
* Add Deprecations for 2.0 (#792)
* Improve IsoDateTimeField test clarity (#790)
* Fix form attr references in tests (#789)
* Simplify tox config, drop python 3.3 & django 1.8 (#787)
* Make get_filter_name a classmethod, allowing it to be overriden for each FilterClass (#775)
* Support active timezone (#750)
* Docs Typo: django_filters -> filters in docs (#773)
* Add Polish translations for some messages (#771)
* Remove support for Django 1.9 (EOL) (#752)
* Use required attribute from field when getting schema fields (#766)
* Prevent circular ImportError hiding for rest_framework sub-package (#741)
* Deprecate 'extra' field attrs on Filter (#734)
* Add SuffixedMultiWidget (#681)
* Fix null filtering for *Choice filters (#680)
* Use isort on imports (#761)
* Use urlencode from django.utils.http (#760)
* Remove OrderingFilter.help_text (#757)
* Update DRF test dependency to 3.6 (#747)
Version 1.0.4 (2017-05-19)
--------------------------
Quick fix for verbose_field_name issue from 1.0.3 (#722)
Version 1.0.3 (2017-05-16)
--------------------------
Improves compatibility with Django REST Framework schema generation.
See the `1.0.3 Milestone`__ for full details.
__ https://github.com/carltongibson/django-filter/milestone/13?closed=1
Version 1.0.2 (2017-03-20)
--------------------------
Updates for compatibility with Django 1.11 and Django REST Framework 3.6.
Adds CI testing against Python 3.6
See the `1.0.2 Milestone`__ for full details.
__ https://github.com/carltongibson/django-filter/milestone/12?closed=1
Version 1.0.1 (2016-11-28)
--------------------------
Small release to ease compatibility with DRF:
* #568 Adds ``rest_framework`` to the ``django_filters`` namespace to allow single
``import django_filters` usage.
* A number of small updates to the docs
Version 1.0 (2016-11-17)
------------------------
This release removes all the deprecated code from 0.14 and 0.15 for 1.0 #480.
Please see the `Migration Notes`__ for details of how to migrate.
Stick with 0.15.3 if you're not ready to update.
__ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt
The release includes a number of small fixes and documentation updates.
See the `1.0 Milestone`__ for full details.
__ https://github.com/carltongibson/django-filter/milestone/8?closed=1
Version 0.15.3 (2016-10-17)
---------------------------
Adds compatibility for DRF (3.5+) get_schema_fields filter backend
introspection.
* #492 Port get_schema_fields from DRF
Version 0.15.2 (2016-09-29)
---------------------------
* #507 Fix compatibility issue when not using the DTL
Version 0.15.1 (2016-09-28)
---------------------------
A couple of quick bug fixes:
* #496 OrderingFilter not working with Select widget
* #498 DRF Backend Templates not loading
Version 0.15.0 (2016-09-20)
---------------------------
This is a preparatory release for a 1.0. Lots of clean-up, lots of changes,
mostly backwards compatible.
Special thanks to Ryan P Kilby (@rpkilby) for lots of hard work.
Most changes should raise a Deprecation Warning.
**Note**: if you're doing *Clever Things™* with the various filter options
— ``filter_overrides`` etc — you may run into an `AttributeError` since these
are now defined on the metaclass and not on the filter itself.
(See the discussion on #459)
Summary: Highly Recommended, but take a moment to ensure everything still works.
* Added the DRF backend. #481
* Deprecated `MethodFilter` in favour of `Filter.method` #382
* Move filter options to metaclass #459
* Added `get_filter_predicate` hook. (Allows e.g. filtering on annotated fields) #469
* Rework Ordering options into a filter #472
* Hardened all deprecations for 1.0. Please do see the `Migration Notes`__
__ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt
Version 0.14.0 (2016-08-14)
---------------------------
* Confirmed support for Django 1.10.
* Add support for filtering on DurationField (new in Django 1.8).
* Fix UUIDFilter import issue
* Improve FieldLookupError message
* Add filters_for_model to improve extensibility
* Fix limit_choices_to behavior with callables
* Fix distinct behavior for range filters
* Various Minor Clean up issues.
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)
---------------------------
* Raised minimum Django version to 1.8.x
* FEATURE: Add support for custom ORM lookup types #221
* FEATURE: Add JavaScript friendly BooleanWidget #270
* FIXED: (More) Compatability with Django 1.8 and Django 1.9+
* BREAKING CHANGE: custom filter names are now also be used for ordering #230
If you use ordering on a field you defined as custom filter with custom
name, you should now use the filter name as ordering key as well.
Eg. For a filter like :
class F(FilterSet):
account = CharFilter(name='username')
class Meta:
model = User
fields = ['account', 'status']
order_by = True
Before, ordering was like `?o=username`. Since 0.12.0 it's `o=account`.
Version 0.11.0 (2015-08-14)
---------------------------
* FEATURE: Added default filter method lookup for MethodFilter #222
* FEATURE: Added support for yesterday in daterangefilter #234
* FEATURE: Created Filter for NumericRange. #236
* FEATURE: Added Date/time range filters #215
* FEATURE: Added option to raise with `strict` #255
* FEATURE: Added Form Field and Filter to parse ISO-8601 timestamps
Version 0.10.0 (2015-05-13)
---------------------
* FEATURE: Added ``conjoined`` parameter to ``MultipleChoiceFilter``
* FEATURE: Added ``together`` meta option to validate fields as a group
* FIXED: Added testing on Django 1.8
* FIXED: ``get_model_field`` on Django 1.8
Version 0.9.2 (2015-01-23)
--------------------------
* FIXED: Compatibility with Django v1.8a1
Version 0.9.1 (2014-12-03)
--------------------------
* FIXED: Compatibility with Debug Toolbar's versions panel
Version 0.9 (2014-11-28)
------------------------
* FEATURE: Allow Min/Max-Only use of RangeFilter
* FEATURE: Added TypedChoiceFilter
* FIXED: Correct logic for short circuit on MultipleChoiceFilter
Added `always_filter` attribute and `is_noop()` test to apply short-circuiting.
Set `always_filter` to `False` on init to apply default `is_noop()` test.
Override `is_noop()` for more complex cases.
* MISC: Version bumping with ``bumpversion``
Version 0.8 (2014-09-29)
------------------------
* FEATURE: Added exclusion filters support
* FEATURE: Added `fields` dictionary shorthand syntax
* FEATURE: Added `MethodFilter`.
* FIXED: #115 "filters.Filter.filter() fails if it receives [] or () as value"
* MISC: Various Documentation and Testing improvements
Version 0.7 (2013-08-10)
------------------------
* FEATURE: Added support for AutoField.
* FEATURE: There is a "distinct" flag to ensure that only unique rows are
returned.
* FEATURE: Support descending ordering (slighty backwards incompatible).
* FEATURE: Support "strict" querysets, ie wrong filter data returns no results.
* FIXED: Some translation strings were changed to be in line with admin.
* FIXED: Support for Django 1.7.
Version 0.6 (2013-03-25)
------------------------
* raised minimum Django version to 1.4.x
* added Python 3.2 and Python 3.3 support
* added Django 1.5 support and initial 1.6 compatability
* FEATURE: recognition of custom model field subclasses
* FEATURE: allow optional display names for order_by values
* FEATURE: addition of class-based FilterView
* FEATURE: addition of count() method on FilterSet to prevent pagination
from loading entire queryset
* FIXED: attempts to filter on reverse side of m2m, o2o or fk would
raise an error
Version 0.5.4 (2012-11-16)
--------------------------
* project brought back to life

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
Copyright (c) Alex Gaynor and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
MANIFEST.in Normal file
View File

@ -0,0 +1,14 @@
include AUTHORS
include CHANGES.rst
include LICENSE
include README.rst
include runshell.py
include runtests.py
recursive-include docs *
recursive-include requirements *
recursive-include tests *
recursive-include django_filters/locale *
recursive-include django_filters/templates *.html
prune docs/_build
global-exclude __pycache__
global-exclude *.py[co]

130
PKG-INFO Normal file
View File

@ -0,0 +1,130 @@
Metadata-Version: 1.1
Name: django-filter
Version: 1.1.0
Summary: Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
Home-page: https://github.com/carltongibson/django-filter/tree/master
Author: Carlton Gibson
Author-email: carlton.gibson@noumenal.es
License: BSD
Description: Django Filter
=============
Django-filter is a reusable Django application allowing users to declaratively
add dynamic ``QuerySet`` filtering from URL parameters.
Full documentation on `read the docs`_.
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
:target: https://travis-ci.org/carltongibson/django-filter
.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/carltongibson/django-filter
.. image:: https://badge.fury.io/py/django-filter.svg
:target: http://badge.fury.io/py/django-filter
Requirements
------------
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.8, 1.10, 1.11
* **DRF**: 3.7
Installation
------------
Install using pip:
.. code-block:: sh
pip install django-filter
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. code-block:: python
INSTALLED_APPS = [
...
'django_filters',
]
Usage
-----
Django-filter can be used for generating interfaces similar to the Django
admin's ``list_filter`` interface. It has an API very similar to Django's
``ModelForms``. For example, if you had a Product model you could have a
filterset for it with the code:
.. code-block:: python
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['name', 'price', 'manufacturer']
And then in your view you could do:
.. code-block:: python
def product_list(request):
filter = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': filter})
Usage with Django REST Framework
--------------------------------
Django-filter provides a custom ``FilterSet`` and filter backend for use with
Django REST Framework.
To use this adjust your import to use
``django_filters.rest_framework.FilterSet``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
For more details see the `DRF integration docs`_.
Support
-------
If you have questions about usage or development you can join the
`mailing list`_.
.. _`read the docs`: https://django-filter.readthedocs.io/en/develop/
.. _`mailing list`: http://groups.google.com/group/django-filter
.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Framework :: Django
Classifier: Framework :: Django :: 1.8
Classifier: Framework :: Django :: 1.10
Classifier: Framework :: Django :: 1.11
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: Django

102
README.rst Normal file
View File

@ -0,0 +1,102 @@
Django Filter
=============
Django-filter is a reusable Django application allowing users to declaratively
add dynamic ``QuerySet`` filtering from URL parameters.
Full documentation on `read the docs`_.
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
:target: https://travis-ci.org/carltongibson/django-filter
.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/carltongibson/django-filter
.. image:: https://badge.fury.io/py/django-filter.svg
:target: http://badge.fury.io/py/django-filter
Requirements
------------
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.8, 1.10, 1.11
* **DRF**: 3.7
Installation
------------
Install using pip:
.. code-block:: sh
pip install django-filter
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. code-block:: python
INSTALLED_APPS = [
...
'django_filters',
]
Usage
-----
Django-filter can be used for generating interfaces similar to the Django
admin's ``list_filter`` interface. It has an API very similar to Django's
``ModelForms``. For example, if you had a Product model you could have a
filterset for it with the code:
.. code-block:: python
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['name', 'price', 'manufacturer']
And then in your view you could do:
.. code-block:: python
def product_list(request):
filter = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': filter})
Usage with Django REST Framework
--------------------------------
Django-filter provides a custom ``FilterSet`` and filter backend for use with
Django REST Framework.
To use this adjust your import to use
``django_filters.rest_framework.FilterSet``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
For more details see the `DRF integration docs`_.
Support
-------
If you have questions about usage or development you can join the
`mailing list`_.
.. _`read the docs`: https://django-filter.readthedocs.io/en/develop/
.. _`mailing list`: http://groups.google.com/group/django-filter
.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html

View File

@ -0,0 +1,130 @@
Metadata-Version: 1.1
Name: django-filter
Version: 1.1.0
Summary: Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
Home-page: https://github.com/carltongibson/django-filter/tree/master
Author: Carlton Gibson
Author-email: carlton.gibson@noumenal.es
License: BSD
Description: Django Filter
=============
Django-filter is a reusable Django application allowing users to declaratively
add dynamic ``QuerySet`` filtering from URL parameters.
Full documentation on `read the docs`_.
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
:target: https://travis-ci.org/carltongibson/django-filter
.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/carltongibson/django-filter
.. image:: https://badge.fury.io/py/django-filter.svg
:target: http://badge.fury.io/py/django-filter
Requirements
------------
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.8, 1.10, 1.11
* **DRF**: 3.7
Installation
------------
Install using pip:
.. code-block:: sh
pip install django-filter
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. code-block:: python
INSTALLED_APPS = [
...
'django_filters',
]
Usage
-----
Django-filter can be used for generating interfaces similar to the Django
admin's ``list_filter`` interface. It has an API very similar to Django's
``ModelForms``. For example, if you had a Product model you could have a
filterset for it with the code:
.. code-block:: python
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['name', 'price', 'manufacturer']
And then in your view you could do:
.. code-block:: python
def product_list(request):
filter = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': filter})
Usage with Django REST Framework
--------------------------------
Django-filter provides a custom ``FilterSet`` and filter backend for use with
Django REST Framework.
To use this adjust your import to use
``django_filters.rest_framework.FilterSet``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
For more details see the `DRF integration docs`_.
Support
-------
If you have questions about usage or development you can join the
`mailing list`_.
.. _`read the docs`: https://django-filter.readthedocs.io/en/develop/
.. _`mailing list`: http://groups.google.com/group/django-filter
.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Framework :: Django
Classifier: Framework :: Django :: 1.8
Classifier: Framework :: Django :: 1.10
Classifier: Framework :: Django :: 1.11
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Framework :: Django

View File

@ -0,0 +1,90 @@
AUTHORS
CHANGES.rst
LICENSE
MANIFEST.in
README.rst
runshell.py
runtests.py
setup.cfg
setup.py
django_filter.egg-info/PKG-INFO
django_filter.egg-info/SOURCES.txt
django_filter.egg-info/dependency_links.txt
django_filter.egg-info/not-zip-safe
django_filter.egg-info/top_level.txt
django_filters/__init__.py
django_filters/compat.py
django_filters/conf.py
django_filters/constants.py
django_filters/exceptions.py
django_filters/fields.py
django_filters/filters.py
django_filters/filterset.py
django_filters/models.py
django_filters/utils.py
django_filters/views.py
django_filters/widgets.py
django_filters/locale/de/LC_MESSAGES/django.mo
django_filters/locale/de/LC_MESSAGES/django.po
django_filters/locale/es_AR/LC_MESSAGES/django.mo
django_filters/locale/es_AR/LC_MESSAGES/django.po
django_filters/locale/es_ES/LC_MESSAGES/django.mo
django_filters/locale/es_ES/LC_MESSAGES/django.po
django_filters/locale/fr/LC_MESSAGES/django.mo
django_filters/locale/fr/LC_MESSAGES/django.po
django_filters/locale/pl/LC_MESSAGES/django.mo
django_filters/locale/pl/LC_MESSAGES/django.po
django_filters/locale/ru/LC_MESSAGES/django.mo
django_filters/locale/ru/LC_MESSAGES/django.po
django_filters/locale/zh_CN/LC_MESSAGES/django.po
django_filters/rest_framework/__init__.py
django_filters/rest_framework/backends.py
django_filters/rest_framework/filters.py
django_filters/rest_framework/filterset.py
django_filters/templates/django_filters/rest_framework/crispy_form.html
django_filters/templates/django_filters/rest_framework/form.html
django_filters/templates/django_filters/widgets/multiwidget.html
docs/.DS_Store
docs/Makefile
docs/conf.py
docs/index.txt
docs/make.bat
docs/assets/form.png
docs/dev/tests.txt
docs/guide/install.txt
docs/guide/migration.txt
docs/guide/rest_framework.txt
docs/guide/tips.txt
docs/guide/usage.txt
docs/ref/fields.txt
docs/ref/filters.txt
docs/ref/filterset.txt
docs/ref/settings.txt
docs/ref/widgets.txt
requirements/maintainer.txt
requirements/test-ci.txt
requirements/test.txt
tests/__init__.py
tests/models.py
tests/settings.py
tests/tags
tests/test_conf.py
tests/test_deprecations.py
tests/test_fields.py
tests/test_filtering.py
tests/test_filters.py
tests/test_filterset.py
tests/test_forms.py
tests/test_utils.py
tests/test_views.py
tests/test_widgets.py
tests/urls.py
tests/rest_framework/__init__.py
tests/rest_framework/apps.py
tests/rest_framework/models.py
tests/rest_framework/test_backends.py
tests/rest_framework/test_filters.py
tests/rest_framework/test_filterset.py
tests/rest_framework/test_integration.py
tests/rest_framework/templates/filter_template.html
tests/templates/tests/book_filter.html

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
django_filters

View File

@ -0,0 +1,34 @@
# flake8: noqa
from __future__ import absolute_import
import pkgutil
from .constants import STRICTNESS
from .filterset import FilterSet
from .filters import *
# We make the `rest_framework` module available without an additional import.
# If DRF is not installed, no-op.
if pkgutil.find_loader('rest_framework') is not None:
from . import rest_framework
del pkgutil
__version__ = '1.1.0'
def parse_version(version):
'''
'0.1.2-dev' -> (0, 1, 2, 'dev')
'0.1.2' -> (0, 1, 2)
'''
v = version.split('.')
v = v[:-1] + v[-1].split('-')
ret = []
for p in v:
if p.isdigit():
ret.append(int(p))
else:
ret.append(p)
return tuple(ret)
VERSION = parse_version(__version__)

71
django_filters/compat.py Normal file
View File

@ -0,0 +1,71 @@
from __future__ import absolute_import
import django
from django.conf import settings
from django.utils.timezone import make_aware as make_aware_orig
try:
from django.forms.utils import pretty_name
except ImportError: # Django 1.8
from django.forms.forms import pretty_name
# django-crispy-forms is optional
try:
import crispy_forms
except ImportError:
crispy_forms = None
def is_crispy():
return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
# coreapi is optional (Note that uritemplate is a dependency of coreapi)
# Fixes #525 - cannot simply import from rest_framework.compat, due to
# import issues w/ django-guardian.
try:
import coreapi
except ImportError:
coreapi = None
try:
import coreschema
except ImportError:
coreschema = None
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
def remote_queryset(field):
model = remote_model(field)
limit_choices_to = field.get_limit_choices_to()
return model._default_manager.complex_filter(limit_choices_to)
def format_value(widget, value):
if django.VERSION >= (1, 10):
return widget.format_value(value)
return widget._format_value(value)
def make_aware(value, timezone, is_dst):
"""is_dst was added for 1.9"""
if django.VERSION >= (1, 9):
return make_aware_orig(value, timezone, is_dst)
else:
return make_aware_orig(value, timezone)

117
django_filters/conf.py Normal file
View File

@ -0,0 +1,117 @@
from __future__ import absolute_import
from django.conf import settings as dj_settings
from django.core.signals import setting_changed
from django.utils.translation import ugettext_lazy as _
from .constants import STRICTNESS
from .utils import deprecate
DEFAULTS = {
'DISABLE_HELP_TEXT': False,
'HELP_TEXT_FILTER': True,
'HELP_TEXT_EXCLUDE': True,
# empty/null choices
'EMPTY_CHOICE_LABEL': '---------',
'NULL_CHOICE_LABEL': None,
'NULL_CHOICE_VALUE': 'null',
'STRICTNESS': STRICTNESS.RETURN_NO_RESULTS,
'VERBOSE_LOOKUPS': {
# transforms don't need to be verbose, since their expressions are chained
'date': _('date'),
'year': _('year'),
'month': _('month'),
'day': _('day'),
'week_day': _('week day'),
'hour': _('hour'),
'minute': _('minute'),
'second': _('second'),
# standard lookups
'exact': _(''),
'iexact': _(''),
'contains': _('contains'),
'icontains': _('contains'),
'in': _('is in'),
'gt': _('is greater than'),
'gte': _('is greater than or equal to'),
'lt': _('is less than'),
'lte': _('is less than or equal to'),
'startswith': _('starts with'),
'istartswith': _('starts with'),
'endswith': _('ends with'),
'iendswith': _('ends with'),
'range': _('is in range'),
'isnull': _(''),
'regex': _('matches regex'),
'iregex': _('matches regex'),
'search': _('search'),
# postgres lookups
'contained_by': _('is contained by'),
'overlap': _('overlaps'),
'has_key': _('has key'),
'has_keys': _('has keys'),
'has_any_keys': _('has any keys'),
'trigram_similar': _('search'),
},
}
DEPRECATED_SETTINGS = [
'HELP_TEXT_FILTER',
'HELP_TEXT_EXCLUDE',
]
def is_callable(value):
# check for callables, except types
return callable(value) and not isinstance(value, type)
class Settings(object):
def __getattr__(self, name):
if name not in DEFAULTS:
msg = "'%s' object has no attribute '%s'"
raise AttributeError(msg % (self.__class__.__name__, name))
value = self.get_setting(name)
if is_callable(value):
value = value()
# Cache the result
setattr(self, name, value)
return value
def get_setting(self, setting):
django_setting = 'FILTERS_%s' % setting
if setting in DEPRECATED_SETTINGS and hasattr(dj_settings, django_setting):
deprecate("The '%s' setting has been deprecated." % django_setting)
return getattr(dj_settings, django_setting, DEFAULTS[setting])
def change_setting(self, setting, value, enter, **kwargs):
if not setting.startswith('FILTERS_'):
return
setting = setting[8:] # strip 'FILTERS_'
# ensure a valid app setting is being overridden
if setting not in DEFAULTS:
return
# if exiting, delete value to repopulate
if enter:
setattr(self, setting, value)
else:
delattr(self, setting)
settings = Settings()
setting_changed.connect(settings.change_setting)

View File

@ -0,0 +1,24 @@
ALL_FIELDS = '__all__'
EMPTY_VALUES = ([], (), {}, '', None)
class STRICTNESS(object):
class IGNORE(object):
pass
class RETURN_NO_RESULTS(object):
pass
class RAISE_VALIDATION_ERROR(object):
pass
# Values of False & True chosen for backward compatability reasons.
# Originally, these were the only options.
_LEGACY = {
False: IGNORE,
True: RETURN_NO_RESULTS,
"RAISE": RAISE_VALIDATION_ERROR,
}

View File

@ -0,0 +1,9 @@
from django.core.exceptions import FieldError
class FieldLookupError(FieldError):
def __init__(self, model_field, lookup_expr):
super(FieldLookupError, self).__init__(
"Unsupported lookup '%s' for field '%s'." % (lookup_expr, model_field)
)

294
django_filters/fields.py Normal file
View File

@ -0,0 +1,294 @@
from __future__ import absolute_import, unicode_literals
from collections import namedtuple
from datetime import datetime, time
import django
from django import forms
from django.utils.dateparse import parse_datetime
from django.utils.encoding import force_str
from django.utils.translation import ugettext_lazy as _
from .conf import settings
from .utils import handle_timezone
from .widgets import BaseCSVWidget, CSVWidget, LookupTypeWidget, RangeWidget
class RangeField(forms.MultiValueField):
widget = RangeWidget
def __init__(self, fields=None, *args, **kwargs):
if fields is None:
fields = (
forms.DecimalField(),
forms.DecimalField())
super(RangeField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
return slice(*data_list)
return None
class DateRangeField(RangeField):
def __init__(self, *args, **kwargs):
fields = (
forms.DateField(),
forms.DateField())
super(DateRangeField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
start_date, stop_date = data_list
if start_date:
start_date = handle_timezone(
datetime.combine(start_date, time.min),
False
)
if stop_date:
stop_date = handle_timezone(
datetime.combine(stop_date, time.max),
False
)
return slice(start_date, stop_date)
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):
def __init__(self, *args, **kwargs):
fields = (
forms.TimeField(),
forms.TimeField())
super(TimeRangeField, self).__init__(fields, *args, **kwargs)
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):
def __init__(self, field, lookup_choices, *args, **kwargs):
fields = (
field,
forms.ChoiceField(choices=lookup_choices)
)
defaults = {
'widgets': [f.widget for f in fields],
}
widget = LookupTypeWidget(**defaults)
kwargs['widget'] = widget
kwargs['help_text'] = field.help_text
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if len(data_list) == 2:
return Lookup(value=data_list[0], lookup_type=data_list[1] or 'exact')
return Lookup(value=None, lookup_type='exact')
class IsoDateTimeField(forms.DateTimeField):
"""
Supports 'iso-8601' date format too which is out the scope of
the ``datetime.strptime`` standard library
# ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
Based on Gist example by David Medina https://gist.github.com/copitux/5773821
"""
ISO_8601 = 'iso-8601'
input_formats = [ISO_8601]
def strptime(self, value, format):
value = force_str(value)
if format == self.ISO_8601:
parsed = parse_datetime(value)
if parsed is None: # Continue with other formats if doesn't match
raise ValueError
return handle_timezone(parsed)
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
"""
base_widget_class = BaseCSVWidget
def __init__(self, *args, **kwargs):
widget = kwargs.get('widget') or self.widget
kwargs['widget'] = self._get_widget_class(widget)
super(BaseCSVField, self).__init__(*args, **kwargs)
def _get_widget_class(self, widget):
# passthrough, allows for override
if isinstance(widget, BaseCSVWidget) or (
isinstance(widget, type) and
issubclass(widget, BaseCSVWidget)):
return widget
# complain since we are unable to reconstruct widget instances
assert isinstance(widget, type), \
"'%s.widget' must be a widget class, not %s." \
% (self.__class__.__name__, repr(widget))
bases = (self.base_widget_class, widget, )
return type(str('CSV%s' % widget.__name__), bases, {})
def clean(self, value):
if value is None:
return None
return [super(BaseCSVField, self).clean(v) for v in value]
class BaseRangeField(BaseCSVField):
# Force use of text input, as range must always have two inputs. A date
# input would only allow a user to input one value and would always fail.
widget = CSVWidget
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
class ChoiceIterator(object):
# Emulates the behavior of ModelChoiceIterator, but instead wraps
# the field's _choices iterable.
def __init__(self, field, choices):
self.field = field
self.choices = choices
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for choice in self.choices:
yield choice
def __len__(self):
add = 1 if self.field.empty_label is not None else 0
add += 1 if self.field.null_label is not None else 0
return len(self.choices) + add
class ModelChoiceIterator(forms.models.ModelChoiceIterator):
# Extends the base ModelChoiceIterator to add in 'null' choice handling.
# This is a bit verbose since we have to insert the null choice after the
# empty choice, but before the remainder of the choices.
def __iter__(self):
iterable = super(ModelChoiceIterator, self).__iter__()
if self.field.empty_label is not None:
yield next(iterable)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for value in iterable:
yield value
def __len__(self):
add = 1 if self.field.null_label is not None else 0
return super(ModelChoiceIterator, self).__len__() + add
class ChoiceIteratorMixin(object):
def __init__(self, *args, **kwargs):
self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
super(ChoiceIteratorMixin, self).__init__(*args, **kwargs)
def _get_choices(self):
if django.VERSION >= (1, 11):
return super(ChoiceIteratorMixin, self)._get_choices()
# HACK: Django < 1.11 does not allow a custom iterator to be provided.
# This code only executes for Model*ChoiceFields.
if hasattr(self, '_choices'):
return self._choices
return self.iterator(self)
def _set_choices(self, value):
super(ChoiceIteratorMixin, self)._set_choices(value)
value = self.iterator(self, self._choices)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices)
# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
super(ChoiceField, self).__init__(*args, **kwargs)
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = None
super(MultipleChoiceField, self).__init__(*args, **kwargs)
class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
iterator = ModelChoiceIterator
def to_python(self, value):
# bypass the queryset value check
if self.null_label is not None and value == self.null_value:
return value
return super(ModelChoiceField, self).to_python(value)
class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
iterator = ModelChoiceIterator
def _check_values(self, value):
null = self.null_label is not None and value and self.null_value in value
if null: # remove the null value and any potential duplicates
value = [v for v in value if v != self.null_value]
result = list(super(ModelMultipleChoiceField, self)._check_values(value))
result += [self.null_value] if null else []
return result

738
django_filters/filters.py Normal file
View File

@ -0,0 +1,738 @@
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from datetime import timedelta
from django import forms
from django.db.models import Q
from django.db.models.constants import LOOKUP_SEP
from django.db.models.sql.constants import QUERY_TERMS
from django.utils import six
from django.utils.itercompat import is_iterable
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from .compat import pretty_name
from .conf import settings
from .constants import EMPTY_VALUES
from .fields import (
BaseCSVField,
BaseRangeField,
ChoiceField,
DateRangeField,
DateTimeRangeField,
IsoDateTimeField,
Lookup,
LookupTypeField,
ModelChoiceField,
ModelMultipleChoiceField,
MultipleChoiceField,
RangeField,
TimeRangeField
)
from .utils import deprecate, label_for_filter
__all__ = [
'AllValuesFilter',
'AllValuesMultipleFilter',
'BaseCSVFilter',
'BaseInFilter',
'BaseRangeFilter',
'BooleanFilter',
'CharFilter',
'ChoiceFilter',
'DateFilter',
'DateFromToRangeFilter',
'DateRangeFilter',
'DateTimeFilter',
'DateTimeFromToRangeFilter',
'DurationFilter',
'Filter',
'IsoDateTimeFilter',
'ModelChoiceFilter',
'ModelMultipleChoiceFilter',
'MultipleChoiceFilter',
'NumberFilter',
'NumericRangeFilter',
'OrderingFilter',
'RangeFilter',
'TimeFilter',
'TimeRangeFilter',
'TypedChoiceFilter',
'TypedMultipleChoiceFilter',
'UUIDFilter',
]
LOOKUP_TYPES = sorted(QUERY_TERMS)
def _extra_attr(attr):
fmt = ("The `.%s` attribute has been deprecated in favor of making it accessible "
"alongside the other field kwargs. You should now access it as `.extra['%s']`.")
def fget(self):
deprecate(fmt % (attr, attr))
return self.extra.get(attr)
def fset(self, value):
deprecate(fmt % (attr, attr))
self.extra[attr] = value
return {'fget': fget, 'fset': fset}
class Filter(object):
creation_counter = 0
field_class = forms.Field
def __init__(self, field_name=None, label=None, method=None, lookup_expr='exact',
distinct=False, exclude=False, **kwargs):
self.field_name = field_name
if field_name is None and 'name' in kwargs:
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
self.field_name = kwargs.pop('name')
self.label = label
self.method = method
self.lookup_expr = lookup_expr
self.distinct = distinct
self.exclude = exclude
self.extra = kwargs
self.extra.setdefault('required', False)
self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1
def get_method(self, qs):
"""Return filter method based on whether we're excluding
or simply filtering.
"""
return qs.exclude if self.exclude else qs.filter
def method():
"""
Filter method needs to be lazily resolved, as it may be dependent on
the 'parent' FilterSet.
"""
def fget(self):
return self._method
def fset(self, value):
self._method = value
# clear existing FilterMethod
if isinstance(self.filter, FilterMethod):
del self.filter
# override filter w/ FilterMethod.
if value is not None:
self.filter = FilterMethod(self)
return locals()
method = property(**method())
def name():
def fget(self):
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
return self.field_name
def fset(self, value):
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
self.field_name = value
return locals()
name = property(**name())
def label():
def fget(self):
if self._label is None and hasattr(self, 'parent'):
model = self.parent._meta.model
self._label = label_for_filter(
model, self.field_name, self.lookup_expr, self.exclude
)
return self._label
def fset(self, value):
self._label = value
return locals()
label = property(**label())
# deprecated field props
widget = property(**_extra_attr('widget'))
required = property(**_extra_attr('required'))
@property
def field(self):
if not hasattr(self, '_field'):
field_kwargs = self.extra.copy()
if settings.DISABLE_HELP_TEXT:
field_kwargs.pop('help_text', None)
if (self.lookup_expr is None or
isinstance(self.lookup_expr, (list, tuple))):
lookup = []
for x in LOOKUP_TYPES:
if isinstance(x, (list, tuple)) and len(x) == 2:
choice = (x[0], x[1])
else:
choice = (x, x)
if self.lookup_expr is None:
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(**field_kwargs), lookup,
required=field_kwargs['required'], label=self.label)
else:
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
def filter(self, qs, value):
if isinstance(value, Lookup):
lookup = six.text_type(value.lookup_type)
value = value.value
else:
lookup = self.lookup_expr
if value in EMPTY_VALUES:
return qs
if self.distinct:
qs = qs.distinct()
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, lookup): value})
return qs
class CharFilter(Filter):
field_class = forms.CharField
class BooleanFilter(Filter):
field_class = forms.NullBooleanField
class ChoiceFilter(Filter):
field_class = ChoiceField
def __init__(self, *args, **kwargs):
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
super(ChoiceFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value != self.null_value:
return super(ChoiceFilter, self).filter(qs, value)
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None})
return qs.distinct() if self.distinct else qs
class TypedChoiceFilter(Filter):
field_class = forms.TypedChoiceField
class UUIDFilter(Filter):
field_class = forms.UUIDField
class MultipleChoiceFilter(Filter):
"""
This filter performs OR(by default) or AND(using conjoined=True) query
on the selected options.
Advanced usage
--------------
Depending on your application logic, when all or no choices are selected,
filtering may be a no-operation. In this case you may wish to avoid the
filtering overhead, particularly if using a `distinct` call.
You can override `get_filter_predicate` to use a custom filter.
By default it will use the filter's name for the key, and the value will
be the model object - or in case of passing in `to_field_name` the
value of that attribute on the model.
Set `always_filter` to `False` after instantiation to enable the default
`is_noop` test. You can override `is_noop` if you need a different test
for your application.
`distinct` defaults to `True` as to-many relationships will generally
require this.
"""
field_class = MultipleChoiceField
always_filter = True
def __init__(self, *args, **kwargs):
kwargs.setdefault('distinct', True)
self.conjoined = kwargs.pop('conjoined', False)
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
super(MultipleChoiceFilter, self).__init__(*args, **kwargs)
def is_noop(self, qs, value):
"""
Return `True` to short-circuit unnecessary and potentially slow
filtering.
"""
if self.always_filter:
return False
# A reasonable default for being a noop...
if self.extra.get('required') and len(value) == len(self.field.choices):
return True
return False
def filter(self, qs, value):
if not value:
# Even though not a noop, no point filtering if empty.
return qs
if self.is_noop(qs, value):
return qs
if not self.conjoined:
q = Q()
for v in set(value):
if v == self.null_value:
v = None
predicate = self.get_filter_predicate(v)
if self.conjoined:
qs = self.get_method(qs)(**predicate)
else:
q |= Q(**predicate)
if not self.conjoined:
qs = self.get_method(qs)(q)
return qs.distinct() if self.distinct else qs
def get_filter_predicate(self, v):
try:
return {self.field_name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {self.field_name: v}
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
field_class = forms.TypedMultipleChoiceField
class DateFilter(Filter):
field_class = forms.DateField
class DateTimeFilter(Filter):
field_class = forms.DateTimeField
class IsoDateTimeFilter(DateTimeFilter):
"""
Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
For context see:
* https://code.djangoproject.com/ticket/23448
* https://github.com/tomchristie/django-rest-framework/issues/1338
* https://github.com/alex/django-filter/pull/264
"""
field_class = IsoDateTimeField
class TimeFilter(Filter):
field_class = forms.TimeField
class DurationFilter(Filter):
field_class = forms.DurationField
class QuerySetRequestMixin(object):
"""
Add callable functionality to filters that support the ``queryset``
argument. If the ``queryset`` is callable, then it **must** accept the
``request`` object as a single argument.
This is useful for filtering querysets by properties on the ``request``
object, such as the user.
Example::
def departments(request):
company = request.user.company
return company.department_set.all()
class EmployeeFilter(filters.FilterSet):
department = filters.ModelChoiceFilter(queryset=departments)
...
The above example restricts the set of departments to those in the logged-in
user's associated company.
"""
def __init__(self, *args, **kwargs):
self.queryset = kwargs.get('queryset')
super(QuerySetRequestMixin, self).__init__(*args, **kwargs)
def get_request(self):
try:
return self.parent.request
except AttributeError:
return None
def get_queryset(self, request):
queryset = self.queryset
if callable(queryset):
return queryset(request)
return queryset
@property
def field(self):
request = self.get_request()
queryset = self.get_queryset(request)
if queryset is not None:
self.extra['queryset'] = queryset
return super(QuerySetRequestMixin, self).field
class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
field_class = ModelChoiceField
def __init__(self, *args, **kwargs):
kwargs.setdefault('empty_label', settings.EMPTY_CHOICE_LABEL)
super(ModelChoiceFilter, self).__init__(*args, **kwargs)
class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
field_class = ModelMultipleChoiceField
class NumberFilter(Filter):
field_class = forms.DecimalField
class NumericRangeFilter(Filter):
field_class = RangeField
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__startswith' % self.field_name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__endswith' % self.field_name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
class RangeFilter(Filter):
field_class = RangeField
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__range' % self.field_name
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__gte' % self.field_name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__lte' % self.field_name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
def _truncate(dt):
return dt.date()
class DateRangeFilter(ChoiceFilter):
options = {
'': (_('Any date'), lambda qs, name: qs),
1: (_('Today'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: now().day
})),
2: (_('Past 7 days'), lambda qs, name: qs.filter(**{
'%s__gte' % name: _truncate(now() - timedelta(days=7)),
'%s__lt' % name: _truncate(now() + timedelta(days=1)),
})),
3: (_('This month'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month
})),
4: (_('This year'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
})),
5: (_('Yesterday'), lambda qs, name: qs.filter(**{
'%s__year' % name: now().year,
'%s__month' % name: now().month,
'%s__day' % name: (now() - timedelta(days=1)).day,
})),
}
def __init__(self, *args, **kwargs):
kwargs['choices'] = [
(key, value[0]) for key, value in six.iteritems(self.options)]
# empty/null choices not relevant
kwargs.setdefault('empty_label', None)
kwargs.setdefault('null_label', None)
super(DateRangeFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
try:
value = int(value)
except (ValueError, TypeError):
value = ''
assert value in self.options
qs = self.options[value][1](qs, self.field_name)
if self.distinct:
qs = qs.distinct()
return qs
class DateFromToRangeFilter(RangeFilter):
field_class = DateRangeField
class DateTimeFromToRangeFilter(RangeFilter):
field_class = DateTimeRangeField
class TimeRangeFilter(RangeFilter):
field_class = TimeRangeField
class AllValuesFilter(ChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesFilter, self).field
class AllValuesMultipleFilter(MultipleChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesMultipleFilter, 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):
kwargs.setdefault('help_text', _('Multiple values may be separated by commas.'))
super(BaseCSVFilter, self).__init__(*args, **kwargs)
class ConcreteCSVField(self.base_field_class, self.field_class):
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 OrderingFilter(BaseCSVFilter, ChoiceFilter):
"""
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
two additional arguments that are used to build the ordering choices.
* ``fields`` is a mapping of {model field name: parameter name}. The
parameter names are exposed in the choices and mask/alias the field
names used in the ``order_by()`` call. Similar to field ``choices``,
``fields`` accepts the 'list of two-tuples' syntax that retains order.
``fields`` may also just be an iterable of strings. In this case, the
field names simply double as the exposed parameter names.
* ``field_labels`` is an optional argument that allows you to customize
the display label for the corresponding parameter. It accepts a mapping
of {field name: human readable label}. Keep in mind that the key is the
field name, and not the exposed parameter name.
Additionally, you can just provide your own ``choices`` if you require
explicit control over the exposed options. For example, when you might
want to disable descending sort options.
This filter is also CSV-based, and accepts multiple ordering params. The
default select widget does not enable the use of this, but it is useful
for APIs.
"""
descending_fmt = _('%s (descending)')
def __init__(self, *args, **kwargs):
"""
``fields`` may be either a mapping or an iterable.
``field_labels`` must be a map of field names to display labels
"""
fields = kwargs.pop('fields', {})
fields = self.normalize_fields(fields)
field_labels = kwargs.pop('field_labels', {})
self.param_map = {v: k for k, v in fields.items()}
if 'choices' not in kwargs:
kwargs['choices'] = self.build_choices(fields, field_labels)
kwargs.setdefault('label', _('Ordering'))
kwargs.setdefault('help_text', '')
kwargs.setdefault('null_label', None)
super(OrderingFilter, self).__init__(*args, **kwargs)
def get_ordering_value(self, param):
descending = param.startswith('-')
param = param[1:] if descending else param
field_name = self.param_map.get(param, param)
return "-%s" % field_name if descending else field_name
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
ordering = [self.get_ordering_value(param) for param in value]
return qs.order_by(*ordering)
@classmethod
def normalize_fields(cls, fields):
"""
Normalize the fields into an ordered map of {field name: param name}
"""
# fields is a mapping, copy into new OrderedDict
if isinstance(fields, dict):
return OrderedDict(fields)
# convert iterable of values => iterable of pairs (field name, param name)
assert is_iterable(fields), \
"'fields' must be an iterable (e.g., a list, tuple, or mapping)."
# fields is an iterable of field names
assert all(isinstance(field, six.string_types) or
is_iterable(field) and len(field) == 2 # may need to be wrapped in parens
for field in fields), \
"'fields' must contain strings or (field name, param name) pairs."
return OrderedDict([
(f, f) if isinstance(f, six.string_types) else f for f in fields
])
def build_choices(self, fields, labels):
ascending = [
(param, labels.get(field, _(pretty_name(param))))
for field, param in fields.items()
]
descending = [
('-%s' % param, labels.get('-%s' % param, self.descending_fmt % label))
for param, label in ascending
]
# interleave the ascending and descending choices
return [val for pair in zip(ascending, descending) for val in pair]
class FilterMethod(object):
"""
This helper is used to override Filter.filter() when a 'method' argument
is passed. It proxies the call to the actual method on the filter's parent.
"""
def __init__(self, filter_instance):
self.f = filter_instance
def __call__(self, qs, value):
if value in EMPTY_VALUES:
return qs
return self.method(qs, self.f.field_name, value)
@property
def method(self):
"""
Resolve the method on the parent filterset.
"""
instance = self.f
# noop if 'method' is a function
if callable(instance.method):
return instance.method
# otherwise, method is the name of a method on the parent FilterSet.
assert hasattr(instance, 'parent'), \
"Filter '%s' must have a parent FilterSet to find '.%s()'" % \
(instance.field_name, instance.method)
parent = instance.parent
method = getattr(parent, instance.method, None)
assert callable(method), \
"Expected parent FilterSet '%s.%s' to have a '.%s()' method." % \
(parent.__class__.__module__, parent.__class__.__name__, instance.method)
return method

455
django_filters/filterset.py Normal file
View File

@ -0,0 +1,455 @@
from __future__ import absolute_import, unicode_literals
import copy
from collections import OrderedDict
from django import forms
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.related import ForeignObjectRel
from django.utils import six
from .compat import remote_field, remote_queryset
from .conf import settings
from .constants import ALL_FIELDS, EMPTY_VALUES, STRICTNESS
from .filters import (
BaseInFilter,
BaseRangeFilter,
BooleanFilter,
CharFilter,
ChoiceFilter,
DateFilter,
DateTimeFilter,
DurationFilter,
Filter,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
NumberFilter,
TimeFilter,
UUIDFilter
)
from .utils import (
deprecate,
get_all_model_fields,
get_model_field,
resolve_field,
try_dbfield
)
def _together_valid(form, fieldset):
field_presence = [
form.cleaned_data.get(field) not in EMPTY_VALUES
for field in fieldset
]
if any(field_presence):
return all(field_presence)
return True
def get_full_clean_override(together):
# coerce together to list of pairs
if isinstance(together[0], (six.string_types)):
together = [together]
def full_clean(form):
super(form.__class__, form).full_clean()
message = 'Following fields must be together: %s'
for each in together:
if not _together_valid(form, each):
return form.add_error(None, message % ','.join(each))
return full_clean
class FilterSetOptions(object):
def __init__(self, options=None):
self.model = getattr(options, 'model', None)
self.fields = getattr(options, 'fields', None)
self.exclude = getattr(options, 'exclude', None)
self.filter_overrides = getattr(options, 'filter_overrides', {})
self.strict = getattr(options, 'strict', None)
self.form = getattr(options, 'form', forms.Form)
if hasattr(options, 'together'):
deprecate('The `Meta.together` option has been deprecated in favor of overriding `Form.clean`.', 1)
self.together = getattr(options, 'together', None)
class FilterSetMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
new_class.base_filters = new_class.get_filters()
return new_class
@classmethod
def get_declared_filters(cls, bases, attrs):
filters = [
(filter_name, attrs.pop(filter_name))
for filter_name, obj in list(attrs.items())
if isinstance(obj, Filter)
]
# Default the `filter.field_name` to the attribute name on the filterset
for filter_name, f in filters:
if getattr(f, 'field_name', None) is None:
f.field_name = filter_name
filters.sort(key=lambda x: x[1].creation_counter)
# merge declared filters from base classes
for base in reversed(bases):
if hasattr(base, 'declared_filters'):
filters = [
(name, f) for name, f
in base.declared_filters.items()
if name not in attrs
] + filters
return OrderedDict(filters)
FILTER_FOR_DBFIELD_DEFAULTS = {
models.AutoField: {'filter_class': NumberFilter},
models.CharField: {'filter_class': CharFilter},
models.TextField: {'filter_class': CharFilter},
models.BooleanField: {'filter_class': BooleanFilter},
models.DateField: {'filter_class': DateFilter},
models.DateTimeField: {'filter_class': DateTimeFilter},
models.TimeField: {'filter_class': TimeFilter},
models.DurationField: {'filter_class': DurationFilter},
models.DecimalField: {'filter_class': NumberFilter},
models.SmallIntegerField: {'filter_class': NumberFilter},
models.IntegerField: {'filter_class': NumberFilter},
models.PositiveIntegerField: {'filter_class': NumberFilter},
models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
models.FloatField: {'filter_class': NumberFilter},
models.NullBooleanField: {'filter_class': BooleanFilter},
models.SlugField: {'filter_class': CharFilter},
models.EmailField: {'filter_class': CharFilter},
models.FilePathField: {'filter_class': CharFilter},
models.URLField: {'filter_class': CharFilter},
models.GenericIPAddressField: {'filter_class': CharFilter},
models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
models.UUIDField: {'filter_class': UUIDFilter},
models.OneToOneField: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': remote_field(f).field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ForeignKey: {
'filter_class': ModelChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': remote_field(f).field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ManyToManyField: {
'filter_class': ModelMultipleChoiceFilter,
'extra': lambda f: {
'queryset': remote_queryset(f),
}
},
}
class BaseFilterSet(object):
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
def __init__(self, data=None, queryset=None, prefix=None, strict=None, request=None):
self.is_bound = data is not None
self.data = data or {}
if queryset is None:
queryset = self._meta.model._default_manager.all()
self.queryset = queryset
self.form_prefix = prefix
# What to do on on validation errors
# Fallback to meta, then settings strictness
if strict is None:
strict = self._meta.strict
if strict is None:
strict = settings.STRICTNESS
# transform legacy values
self.strict = STRICTNESS._LEGACY.get(strict, strict)
self.request = request
self.filters = copy.deepcopy(self.base_filters)
for filter_ in self.filters.values():
# propagate the model and filterset to the filters
filter_.model = self._meta.model
filter_.parent = self
@property
def qs(self):
if not hasattr(self, '_qs'):
if not self.is_bound:
self._qs = self.queryset.all()
return self._qs
if not self.form.is_valid():
if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
raise forms.ValidationError(self.form.errors)
elif self.strict == STRICTNESS.RETURN_NO_RESULTS:
self._qs = self.queryset.none()
return self._qs
# else STRICTNESS.IGNORE... ignoring
# start with all the results and filter from there
qs = self.queryset.all()
for name, filter_ in six.iteritems(self.filters):
value = self.form.cleaned_data.get(name)
if value is not None: # valid & clean data
qs = filter_.filter(qs, value)
self._qs = qs
return self._qs
@property
def form(self):
if not hasattr(self, '_form'):
fields = OrderedDict([
(name, filter_.field)
for name, filter_ in six.iteritems(self.filters)])
Form = type(str('%sForm' % self.__class__.__name__),
(self._meta.form,), fields)
if self._meta.together:
Form.full_clean = get_full_clean_override(self._meta.together)
if self.is_bound:
self._form = Form(self.data, prefix=self.form_prefix)
else:
self._form = Form(prefix=self.form_prefix)
return self._form
@classmethod
def get_fields(cls):
"""
Resolve the 'fields' argument that should be used for generating filters on the
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
"""
model = cls._meta.model
fields = cls._meta.fields
exclude = cls._meta.exclude
assert not (fields is None and exclude is None), \
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit " \
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
# Setting exclude with no fields implies all other fields.
if exclude is not None and fields is None:
fields = ALL_FIELDS
# Resolve ALL_FIELDS into all fields for the filterset's model.
if fields == ALL_FIELDS:
fields = get_all_model_fields(model)
# Remove excluded fields
exclude = exclude or []
if not isinstance(fields, dict):
fields = [(f, ['exact']) for f in fields if f not in exclude]
else:
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
return OrderedDict(fields)
@classmethod
def get_filter_name(cls, field_name, lookup_expr):
"""
Combine a field name and lookup expression into a usable filter name.
Exact lookups are the implicit default, so "exact" is stripped from the
end of the filter name.
"""
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
# This also works with transformed exact lookups, such as 'date__exact'
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
return filter_name
@classmethod
def get_filters(cls):
"""
Get all filters for the filterset. This is the combination of declared and
generated filters.
"""
# No model specified - skip filter generation
if not cls._meta.model:
return cls.declared_filters.copy()
# Determine the filters that should be included on the filterset.
filters = OrderedDict()
fields = cls.get_fields()
undefined = []
for field_name, lookups in fields.items():
field = get_model_field(cls._meta.model, field_name)
# warn if the field doesn't exist.
if field is None:
undefined.append(field_name)
# ForeignObjectRel does not support non-exact lookups
if isinstance(field, ForeignObjectRel):
filters[field_name] = cls.filter_for_reverse_field(field, field_name)
continue
for lookup_expr in lookups:
filter_name = cls.get_filter_name(field_name, lookup_expr)
# If the filter is explicitly declared on the class, skip generation
if filter_name in cls.declared_filters:
filters[filter_name] = cls.declared_filters[filter_name]
continue
if field is not None:
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
# filter out declared filters
undefined = [f for f in undefined if f not in cls.declared_filters]
if undefined:
raise TypeError(
"'Meta.fields' contains fields that are not defined on this FilterSet: "
"%s" % ', '.join(undefined)
)
# Add in declared filters. This is necessary since we don't enforce adding
# declared filters to the 'Meta.fields' option
filters.update(cls.declared_filters)
return filters
@classmethod
def filter_for_field(cls, f, field_name, lookup_expr='exact'):
f, lookup_type = resolve_field(f, lookup_expr)
default = {
'field_name': field_name,
'lookup_expr': lookup_expr,
}
filter_class, params = cls.filter_for_lookup(f, lookup_type)
default.update(params)
assert filter_class is not None, (
"%s resolved field '%s' with '%s' lookup to an unrecognized field "
"type %s. Try adding an override to 'Meta.filter_overrides'. See: "
"https://django-filter.readthedocs.io/en/develop/ref/filterset.html#customise-filter-generation-with-filter-overrides"
) % (cls.__name__, field_name, lookup_expr, f.__class__.__name__)
return filter_class(**default)
@classmethod
def filter_for_reverse_field(cls, f, field_name):
rel = remote_field(f.field)
queryset = f.field.model._default_manager.all()
default = {
'field_name': field_name,
'queryset': queryset,
}
if rel.multiple:
return ModelMultipleChoiceFilter(**default)
else:
return ModelChoiceFilter(**default)
@classmethod
def filter_for_lookup(cls, f, lookup_type):
DEFAULTS = dict(cls.FILTER_DEFAULTS)
if hasattr(cls, '_meta'):
DEFAULTS.update(cls._meta.filter_overrides)
data = try_dbfield(DEFAULTS.get, f.__class__) or {}
filter_class = data.get('filter_class')
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 == 'exact' and f.choices:
return ChoiceFilter, {'choices': f.choices}
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
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)):
pass
def filterset_factory(model, fields=ALL_FIELDS):
meta = type(str('Meta'), (object,), {'model': model, 'fields': fields})
filterset = type(str('%sFilterSet' % model._meta.object_name),
(FilterSet,), {'Meta': meta})
return filterset

Binary file not shown.

View File

@ -0,0 +1,48 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-filter\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-08-10 05:34-0500\n"
"PO-Revision-Date: 2013-08-10 12:29+0100\n"
"Last-Translator: Florian Apolloner <florian@apolloner.eu>\n"
"Language-Team: \n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.5.4\n"
#: filters.py:153
msgid "Any date"
msgstr "Alle Daten"
#: filters.py:154
msgid "Today"
msgstr "Heute"
#: filters.py:159
msgid "Past 7 days"
msgstr "Letzte 7 Tage"
#: filters.py:163
msgid "This month"
msgstr "Diesen Monat"
#: filters.py:167
msgid "This year"
msgstr "Dieses Jahr"
#: filterset.py:332 filterset.py:341
#, python-format
msgid "%s (descending)"
msgstr "%s (absteigend)"
#: widgets.py:63
msgid "All"
msgstr "Alle"

Binary file not shown.

View File

@ -0,0 +1,47 @@
# Django Filter translation.
# Copyright (C) 2013
# This file is distributed under the same license as the django_filter package.
# Gonzalo Bustos, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
"PO-Revision-Date: 2015-10-11 20:53-0300\n"
"Last-Translator: Gonzalo Bustos\n"
"Language-Team: Spanish (Argentina)\n"
"Language: es_AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 1.6.10\n"
#: filters.py:51
msgid "This is an exclusion filter"
msgstr "Este es un filtro de exclusión"
#: filters.py:158
msgid "Any date"
msgstr "Cualquier fecha"
#: filters.py:159
msgid "Today"
msgstr "Hoy"
#: filters.py:164
msgid "Past 7 days"
msgstr "Últimos 7 días"
#: filters.py:168
msgid "This month"
msgstr "Este mes"
#: filters.py:172
msgid "This year"
msgstr "Este año"
#: widgets.py:63
msgid "All"
msgstr "Todos"

Binary file not shown.

View File

@ -0,0 +1,185 @@
# Django Filter translation.
# Copyright (C) 2013
# This file is distributed under the same license as the django_filter package.
# Carlos Goce, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-01-26 20:32+0100\n"
"PO-Revision-Date: 2017-01-26 20:52+0100\n"
"Last-Translator: Carlos Goce\n"
"Language-Team: Spanish (España)\n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.11\n"
#: conf.py:26
msgid "date"
msgstr "fecha"
#: conf.py:27
msgid "year"
msgstr "año"
#: conf.py:28
msgid "month"
msgstr "mes"
#: conf.py:29
msgid "day"
msgstr "día"
#: conf.py:30
msgid "week day"
msgstr "día de la semana"
#: conf.py:31
msgid "hour"
msgstr "hora"
#: conf.py:32
msgid "minute"
msgstr "minuto"
#: conf.py:33
msgid "second"
msgstr "segundo"
#: conf.py:38 conf.py:39
msgid "contains"
msgstr "contiene"
#: conf.py:40
msgid "is in"
msgstr "presente en"
#: conf.py:41
msgid "is greater than"
msgstr "mayor que"
#: conf.py:42
msgid "is greater than or equal to"
msgstr "mayor o igual que"
#: conf.py:43
msgid "is less than"
msgstr "menor que"
#: conf.py:44
msgid "is less than or equal to"
msgstr "menor o igual que"
#: conf.py:45 conf.py:46
msgid "starts with"
msgstr "comienza por"
#: conf.py:47 conf.py:48
msgid "ends with"
msgstr "termina por"
#: conf.py:49
msgid "is in range"
msgstr "en el rango"
#: conf.py:51 conf.py:52
msgid "matches regex"
msgstr "coincide con la expresión regular"
#: conf.py:53 conf.py:61
msgid "search"
msgstr "buscar"
#: conf.py:56
msgid "is contained by"
msgstr "contenido en"
#: conf.py:57
msgid "overlaps"
msgstr "solapado"
#: conf.py:58
msgid "has key"
msgstr "contiene la clave"
#: conf.py:59
msgid "has keys"
msgstr "contiene las claves"
#: conf.py:60
msgid "has any keys"
msgstr "contiene alguna de las claves"
#: fields.py:167
msgid "Range query expects two values."
msgstr "Consultar un rango requiere dos valores"
#: filters.py:443
msgid "Any date"
msgstr "Cualquier fecha"
#: filters.py:444
msgid "Today"
msgstr "Hoy"
#: filters.py:449
msgid "Past 7 days"
msgstr "Últimos 7 días"
#: filters.py:453
msgid "This month"
msgstr "Este mes"
#: filters.py:457
msgid "This year"
msgstr "Este año"
#: filters.py:460
msgid "Yesterday"
msgstr "Ayer"
#: filters.py:526
msgid "Multiple values may be separated by commas."
msgstr "Múltiples valores separados por comas."
#: filters.py:605
#, python-format
msgid "%s (descending)"
msgstr "%s (descendente)"
#: filters.py:621
msgid "Ordering"
msgstr "Ordenado"
#: utils.py:220
msgid "exclude"
msgstr "excluye"
#: widgets.py:71
msgid "All"
msgstr "Todo"
#: widgets.py:119
msgid "Unknown"
msgstr "Desconocido"
#: widgets.py:120
msgid "Yes"
msgstr "Sí"
#: widgets.py:121
msgid "No"
msgstr "No"
#: rest_framework/filterset.py:31
#: templates/django_filters/rest_framework/form.html:5
msgid "Submit"
msgstr "Enviar"
#: templates/django_filters/rest_framework/crispy_form.html:4
#: templates/django_filters/rest_framework/form.html:2
msgid "Field filters"
msgstr "Filtros de campo"

Binary file not shown.

View File

@ -0,0 +1,47 @@
# Django Filter translation.
# Copyright (C) 2013
# This file is distributed under the same license as the django_filter package.
# Axel Haustant <noirbizarre@gmail.com>, 2013.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
"PO-Revision-Date: 2013-07-05 19:24+0200\n"
"Last-Translator: Axel Haustant <noirbizarre@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: filters.py:51
msgid "This is an exclusion filter"
msgstr "Ceci est un filtre d'exclusion"
#: filters.py:158
msgid "Any date"
msgstr "Toutes les dates"
#: filters.py:159
msgid "Today"
msgstr "Aujourd'hui"
#: filters.py:164
msgid "Past 7 days"
msgstr "7 derniers jours"
#: filters.py:168
msgid "This month"
msgstr "Ce mois-ci"
#: filters.py:172
msgid "This year"
msgstr "Cette année"
#: widgets.py:63
msgid "All"
msgstr "Tous"

Binary file not shown.

View File

@ -0,0 +1,201 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#: conf.py:35 conf.py:36 conf.py:49
msgid ""
msgstr ""
"Project-Id-Version: django_filters 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-01 17:21+0000\n"
"PO-Revision-Date: 2015-07-25 01:27+0100\n"
"Last-Translator: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
"Language-Team: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 1.5.4\n"
#: conf.py:25
#, fuzzy
#| msgid "Any date"
msgid "date"
msgstr "Dowolna data"
#: conf.py:26
#, fuzzy
#| msgid "This year"
msgid "year"
msgstr "Ten rok"
#: conf.py:27
#, fuzzy
#| msgid "This month"
msgid "month"
msgstr "Ten miesiąc"
#: conf.py:28
#, fuzzy
#| msgid "Today"
msgid "day"
msgstr "Dziś"
#: conf.py:29
msgid "week day"
msgstr "dzień tygodnia"
#: conf.py:30
msgid "hour"
msgstr "godzina"
#: conf.py:31
msgid "minute"
msgstr "minuta"
#: conf.py:32
msgid "second"
msgstr ""
#: conf.py:37 conf.py:38
msgid "contains"
msgstr "zawiera"
#: conf.py:39
msgid "is in"
msgstr "zawiera się w"
#: conf.py:40
msgid "is greater than"
msgstr "powyżej"
#: conf.py:41
msgid "is greater than or equal to"
msgstr "powyżej lub równe"
#: conf.py:42
msgid "is less than"
msgstr "poniżej"
#: conf.py:43
msgid "is less than or equal to"
msgstr "poniżej lub równe"
#: conf.py:44 conf.py:45
msgid "starts with"
msgstr "zaczyna się od"
#: conf.py:46 conf.py:47
msgid "ends with"
msgstr "kończy się na"
#: conf.py:48
msgid "is in range"
msgstr "zawiera się w zakresie"
#: conf.py:50 conf.py:51
msgid "matches regex"
msgstr "pasuje do wyrażenia regularnego"
#: conf.py:52 conf.py:60
msgid "search"
msgstr "szukaj"
#: conf.py:55
msgid "is contained by"
msgstr "zawiera się w"
#: conf.py:56
msgid "overlaps"
msgstr ""
#: conf.py:57
msgid "has key"
msgstr ""
#: conf.py:58
msgid "has keys"
msgstr ""
#: conf.py:59
msgid "has any keys"
msgstr ""
#: fields.py:172
msgid "Range query expects two values."
msgstr ""
#: filters.py:452
msgid "Any date"
msgstr "Dowolna data"
#: filters.py:453
msgid "Today"
msgstr "Dziś"
#: filters.py:458
msgid "Past 7 days"
msgstr "Ostatnie 7 dni"
#: filters.py:462
msgid "This month"
msgstr "Ten miesiąc"
#: filters.py:466
msgid "This year"
msgstr "Ten rok"
#: filters.py:469
msgid "Yesterday"
msgstr "Wczoraj"
#: filters.py:535
msgid "Multiple values may be separated by commas."
msgstr "Wiele wartości można rozdzielić przecinkami"
#: filters.py:614
#, python-format
msgid "%s (descending)"
msgstr "%s (malejąco)"
#: filters.py:630
msgid "Ordering"
msgstr "Sortowanie"
#: rest_framework/filterset.py:34
#: templates/django_filters/rest_framework/form.html:5
msgid "Submit"
msgstr ""
#: templates/django_filters/rest_framework/crispy_form.html:4
#: templates/django_filters/rest_framework/form.html:2
#, fuzzy
#| msgid "Filter"
msgid "Field filters"
msgstr "Filter"
#: utils.py:225
msgid "exclude"
msgstr ""
#: widgets.py:66
msgid "All"
msgstr "Wszystko"
#: widgets.py:173
msgid "Unknown"
msgstr ""
#: widgets.py:174
msgid "Yes"
msgstr "Tak"
#: widgets.py:175
msgid "No"
msgstr "Nie"
#~ msgid "This is an exclusion filter"
#~ msgstr "Jest to filtr wykluczający"

Binary file not shown.

View File

@ -0,0 +1,48 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: django-filter\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2013-08-10 05:34-0500\n"
"PO-Revision-Date: 2016-09-29 11:47+0300\n"
"Last-Translator: Mikhail Mitrofanov <mm@elec.ru>\n"
"Language-Team: \n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Poedit 1.8.9\n"
#: filters.py:153
msgid "Any date"
msgstr "Любая дата"
#: filters.py:154
msgid "Today"
msgstr "Сегодня"
#: filters.py:159
msgid "Past 7 days"
msgstr "Прошедшие 7 дней"
#: filters.py:163
msgid "This month"
msgstr "За этот месяц"
#: filters.py:167
msgid "This year"
msgstr "В этом году"
#: filterset.py:332 filterset.py:341
#, python-format
msgid "%s (descending)"
msgstr "%s (по убыванию)"
#: widgets.py:63
msgid "All"
msgstr "Все"

View File

@ -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
django_filters/models.py Normal file
View File

View File

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

View File

@ -0,0 +1,104 @@
from __future__ import absolute_import
import warnings
from django.template import loader
from django.utils import six
from . import filters, filterset
from .. import compat
class DjangoFilterBackend(object):
default_filter_set = filterset.FilterSet
@property
def template(self):
if compat.is_crispy():
return 'django_filters/rest_framework/crispy_form.html'
return 'django_filters/rest_framework/form.html'
def get_filter_class(self, view, queryset=None):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)
if filter_class:
filter_model = filter_class.Meta.model
assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
return filter_class
if filter_fields:
MetaBase = getattr(self.default_filter_set, 'Meta', object)
class AutoFilterSet(self.default_filter_set):
class Meta(MetaBase):
model = queryset.model
fields = filter_fields
return AutoFilterSet
return None
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset, request=request).qs
return queryset
def to_html(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if not filter_class:
return None
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
template = loader.get_template(self.template)
context = {
'filter': filter_instance
}
return template.render(context, request)
def get_coreschema_field(self, field):
if isinstance(field, filters.NumberFilter):
field_cls = compat.coreschema.Number
else:
field_cls = compat.coreschema.String
return field_cls(
description=six.text_type(field.extra.get('help_text', ''))
)
def get_schema_fields(self, view):
# This is not compatible with widgets where the query param differs from the
# filter's attribute name. Notably, this includes `MultiWidget`, where query
# params will be of the format `<name>_0`, `<name>_1`, etc...
assert compat.coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
assert compat.coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
filter_class = getattr(view, 'filter_class', None)
if filter_class is None:
try:
filter_class = self.get_filter_class(view, view.get_queryset())
except Exception:
warnings.warn(
"{} is not compatible with schema generation".format(view.__class__)
)
filter_class = None
return [] if not filter_class else [
compat.coreapi.Field(
name=field_name,
required=field.extra['required'],
location='query',
schema=self.get_coreschema_field(field)
) for field_name, field in filter_class.base_filters.items()
]

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}

255
django_filters/utils.py Normal file
View File

@ -0,0 +1,255 @@
import warnings
import django
from django.conf import settings
from django.core.exceptions import FieldError
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, RelatedField
from django.forms import ValidationError
from django.utils import six, timezone
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
from .compat import make_aware, remote_field, remote_model
from .exceptions import FieldLookupError
def deprecate(msg, level_modifier=0):
warnings.warn(
"%s See: https://django-filter.readthedocs.io/en/develop/migration.html" % msg,
DeprecationWarning, stacklevel=3 + level_modifier)
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_all_model_fields(model):
opts = model._meta
return [
f.name for f in sorted(opts.fields + opts.many_to_many)
if not isinstance(f, models.AutoField) and
not (getattr(remote_field(f), 'parent_link', False))
]
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')
"""
fields = get_field_parts(model, field_name)
return fields[-1] if fields else None
def get_field_parts(model, field_name):
"""
Get the field parts that represent the traversable relationships from the
base ``model`` to the final field, described by ``field_name``.
ex::
>>> parts = get_field_parts(Book, 'author__first_name')
>>> [p.verbose_name for p in parts]
['author', 'first name']
"""
parts = field_name.split(LOOKUP_SEP)
opts = model._meta
fields = []
# walk relationships
for name in parts:
try:
field = opts.get_field(name)
except FieldDoesNotExist:
return None
fields.append(field)
if isinstance(field, RelatedField):
opts = remote_model(field)._meta
elif isinstance(field, ForeignObjectRel):
opts = field.related_model._meta
return fields
def resolve_field(model_field, lookup_expr):
"""
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
try:
while lookups:
name = lookups[0]
args = (lhs, name)
if django.VERSION < (2, 0):
# rest_of_lookups was removed in Django 2.0
args += (lookups,)
# 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(*args)
final_lookup = lhs.get_lookup('exact')
return lhs.output_field, final_lookup.lookup_name
lhs = query.try_transform(*args)
lookups = lookups[1:]
except FieldError as e:
six.raise_from(FieldLookupError(model_field, lookup_expr), e)
def handle_timezone(value, is_dst=None):
if settings.USE_TZ and timezone.is_naive(value):
return make_aware(value, timezone.get_current_timezone(), is_dst)
elif not settings.USE_TZ and timezone.is_aware(value):
return timezone.make_naive(value, timezone.utc)
return value
def verbose_field_name(model, field_name):
"""
Get the verbose name for a given ``field_name``. The ``field_name``
will be traversed across relationships. Returns '[invalid name]' for
any field name that cannot be traversed.
ex::
>>> verbose_field_name(Article, 'author__name')
'author name'
"""
if field_name is None:
return '[invalid name]'
parts = get_field_parts(model, field_name)
if not parts:
return '[invalid name]'
names = []
for part in parts:
if isinstance(part, ForeignObjectRel):
if part.related_name:
names.append(part.related_name.replace('_', ' '))
else:
return '[invalid name]'
else:
names.append(force_text(part.verbose_name))
return ' '.join(names)
def verbose_lookup_expr(lookup_expr):
"""
Get a verbose, more humanized expression for a given ``lookup_expr``.
Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
dictionary. Missing keys will simply default to itself.
ex::
>>> verbose_lookup_expr('year__lt')
'year is less than'
# with `FILTERS_VERBOSE_LOOKUPS = {}`
>>> verbose_lookup_expr('year__lt')
'year lt'
"""
from .conf import settings as app_settings
VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
lookups = [
force_text(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
for lookup in lookup_expr.split(LOOKUP_SEP)
]
return ' '.join(lookups)
def label_for_filter(model, field_name, lookup_expr, exclude=False):
"""
Create a generic label suitable for a filter.
ex::
>>> label_for_filter(Article, 'author__name', 'in')
'auther name is in'
"""
name = verbose_field_name(model, field_name)
verbose_expression = [_('exclude'), name] if exclude else [name]
# iterable lookups indicate a LookupTypeField, which should not be verbose
if isinstance(lookup_expr, six.string_types):
verbose_expression += [verbose_lookup_expr(lookup_expr)]
verbose_expression = [force_text(part) for part in verbose_expression if part]
verbose_expression = capfirst(' '.join(verbose_expression))
return verbose_expression
def raw_validation(error):
"""
Deconstruct a django.forms.ValidationError into a primitive structure
eg, plain dicts and lists.
"""
if isinstance(error, ValidationError):
if hasattr(error, 'error_dict'):
error = error.error_dict
elif not hasattr(error, 'message'):
error = error.error_list
else:
error = error.message
if isinstance(error, dict):
return {key: raw_validation(value) for key, value in error.items()}
elif isinstance(error, list):
return [raw_validation(value) for value in error]
else:
return error

101
django_filters/views.py Normal file
View File

@ -0,0 +1,101 @@
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.views.generic import View
from django.views.generic.list import (
MultipleObjectMixin,
MultipleObjectTemplateResponseMixin
)
from .constants import ALL_FIELDS
from .filterset import filterset_factory
class FilterMixin(object):
"""
A mixin that provides a way to show and handle a FilterSet in a request.
"""
filterset_class = None
filter_fields = ALL_FIELDS
def get_filterset_class(self):
"""
Returns the filterset class to use in this view
"""
if self.filterset_class:
return self.filterset_class
elif self.model:
return filterset_factory(model=self.model, fields=self.filter_fields)
else:
msg = "'%s' must define 'filterset_class' or 'model'"
raise ImproperlyConfigured(msg % self.__class__.__name__)
def get_filterset(self, filterset_class):
"""
Returns an instance of the filterset to be used in this view.
"""
kwargs = self.get_filterset_kwargs(filterset_class)
return filterset_class(**kwargs)
def get_filterset_kwargs(self, filterset_class):
"""
Returns the keyword arguments for instanciating the filterset.
"""
kwargs = {
'data': self.request.GET or None,
'request': self.request,
}
try:
kwargs.update({
'queryset': self.get_queryset(),
})
except ImproperlyConfigured:
# ignore the error here if the filterset has a model defined
# to acquire a queryset from
if filterset_class._meta.model is None:
msg = ("'%s' does not define a 'model' and the view '%s' does "
"not return a valid queryset from 'get_queryset'. You "
"must fix one of them.")
args = (filterset_class.__name__, self.__class__.__name__)
raise ImproperlyConfigured(msg % args)
return kwargs
class BaseFilterView(FilterMixin, MultipleObjectMixin, View):
def get(self, request, *args, **kwargs):
filterset_class = self.get_filterset_class()
self.filterset = self.get_filterset(filterset_class)
self.object_list = self.filterset.qs
context = self.get_context_data(filter=self.filterset,
object_list=self.object_list)
return self.render_to_response(context)
class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView):
"""
Render some list of objects with filter, set by `self.model` or
`self.queryset`.
`self.queryset` can actually be any iterable of items, not just a queryset.
"""
template_name_suffix = '_filter'
def object_filter(request, model=None, queryset=None, template_name=None,
extra_context=None, context_processors=None,
filter_class=None):
class ECFilterView(FilterView):
"""Handle the extra_context from the functional object_filter view"""
def get_context_data(self, **kwargs):
context = super(ECFilterView, self).get_context_data(**kwargs)
extra_context = self.kwargs.get('extra_context') or {}
for k, v in extra_context.items():
if callable(v):
v = v()
context[k] = v
return context
kwargs = dict(model=model, queryset=queryset, template_name=template_name,
filterset_class=filter_class)
view = ECFilterView.as_view(**kwargs)
return view(request, extra_context=extra_context)

269
django_filters/widgets.py Normal file
View File

@ -0,0 +1,269 @@
from __future__ import absolute_import, unicode_literals
from collections import Iterable
from itertools import chain
from re import search, sub
import django
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms.utils import flatatt
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.six import string_types
from django.utils.translation import ugettext as _
from .compat import format_value
class LinkWidget(forms.Widget):
def __init__(self, attrs=None, choices=()):
super(LinkWidget, self).__init__(attrs)
self.choices = choices
def value_from_datadict(self, data, files, name):
value = super(LinkWidget, self).value_from_datadict(data, files, name)
self.data = data
return value
def render(self, name, value, attrs=None, choices=()):
if not hasattr(self, 'data'):
self.data = {}
if value is None:
value = ''
if django.VERSION < (1, 11):
final_attrs = self.build_attrs(attrs)
else:
final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
output = ['<ul%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value], name)
if options:
output.append(options)
output.append('</ul>')
return mark_safe('\n'.join(output))
def render_options(self, choices, selected_choices, name):
selected_choices = set(force_text(v) for v in selected_choices)
output = []
for option_value, option_label in chain(self.choices, choices):
if isinstance(option_label, (list, tuple)):
for option in option_label:
output.append(
self.render_option(name, selected_choices, *option))
else:
output.append(
self.render_option(name, selected_choices,
option_value, option_label))
return '\n'.join(output)
def render_option(self, name, selected_choices,
option_value, option_label):
option_value = force_text(option_value)
if option_label == BLANK_CHOICE_DASH[0][1]:
option_label = _("All")
data = self.data.copy()
data[name] = option_value
selected = data == self.data or option_value in selected_choices
try:
url = data.urlencode()
except AttributeError:
url = urlencode(data)
return self.option_string() % {
'attrs': selected and ' class="selected"' or '',
'query_string': url,
'label': force_text(option_label)
}
def option_string(self):
return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
class SuffixedMultiWidget(forms.MultiWidget):
"""
A MultiWidget that allows users to provide custom suffixes instead of indexes.
- Suffixes must be unique.
- There must be the same number of suffixes as fields.
"""
suffixes = []
def __init__(self, *args, **kwargs):
super(SuffixedMultiWidget, self).__init__(*args, **kwargs)
assert len(self.widgets) == len(self.suffixes)
assert len(self.suffixes) == len(set(self.suffixes))
def suffixed(self, name, suffix):
return '_'.join([name, suffix]) if suffix else name
def get_context(self, name, value, attrs):
context = super(SuffixedMultiWidget, self).get_context(name, value, attrs)
for subcontext, suffix in zip(context['widget']['subwidgets'], self.suffixes):
subcontext['name'] = self.suffixed(name, suffix)
return context
def value_from_datadict(self, data, files, name):
return [
widget.value_from_datadict(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
]
def value_omitted_from_data(self, data, files, name):
return all(
widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
)
# Django < 1.11 compat
def format_output(self, rendered_widgets):
rendered_widgets = [
self.replace_name(output, i)
for i, output in enumerate(rendered_widgets)
]
return '\n'.join(rendered_widgets)
def replace_name(self, output, index):
result = search(r'name="(?P<name>.*)_%d"' % index, output)
name = result.group('name')
name = self.suffixed(name, self.suffixes[index])
name = 'name="%s"' % name
return sub(r'name=".*_%d"' % index, name, output)
def decompress(self, value):
if value is None:
return [None, None]
return value
class RangeWidget(forms.MultiWidget):
template_name = 'django_filters/widgets/multiwidget.html'
def __init__(self, attrs=None):
widgets = (forms.TextInput, forms.TextInput)
super(RangeWidget, self).__init__(widgets, attrs)
def format_output(self, rendered_widgets):
# Method was removed in Django 1.11.
return '-'.join(rendered_widgets)
def decompress(self, value):
if value:
return [value.start, value.stop]
return [None, None]
class LookupTypeWidget(forms.MultiWidget):
def decompress(self, value):
if value is None:
return [None, None]
return value
class BooleanWidget(forms.Select):
"""Convert true/false values into the internal Python True/False.
This can be used for AJAX queries that pass true/false from JavaScript's
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):
value = data.get(name, None)
if isinstance(value, string_types):
value = value.lower()
return {
'1': True,
'0': False,
'true': True,
'false': False,
True: True,
False: False,
}.get(value, None)
class BaseCSVWidget(forms.Widget):
def _isiterable(self, value):
return isinstance(value, Iterable) and not isinstance(value, string_types)
def value_from_datadict(self, data, files, name):
value = super(BaseCSVWidget, self).value_from_datadict(data, files, name)
if value is not None:
if value == '': # empty value should parse as an empty list
return []
return value.split(',')
return None
def render(self, name, value, attrs=None):
if not self._isiterable(value):
value = [value]
if len(value) <= 1:
# delegate to main widget (Select, etc...) if not multiple values
value = value[0] if value else ''
return super(BaseCSVWidget, self).render(name, value, attrs)
# if we have multiple values, we need to force render as a text input
# (otherwise, the additional values are lost)
surrogate = forms.TextInput()
value = [force_text(format_value(surrogate, v)) for v in value]
value = ','.join(list(value))
return surrogate.render(name, value, attrs)
class CSVWidget(BaseCSVWidget, forms.TextInput):
pass
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
"""
Enables request query array notation that might be consumed by MultipleChoiceFilter
1. Values can be provided as csv string: ?foo=bar,baz
2. Values can be provided as query array: ?foo[]=bar&foo[]=baz
3. Values can be provided as query array: ?foo=bar&foo=baz
Note: Duplicate and empty values are skipped from results
"""
def value_from_datadict(self, data, files, name):
if not isinstance(data, MultiValueDict):
for key, value in data.items():
# treat value as csv string: ?foo=1,2
if isinstance(value, string_types):
data[key] = [x.strip() for x in value.rstrip(',').split(',') if x]
data = MultiValueDict(data)
values_list = data.getlist(name, data.getlist('%s[]' % name)) or []
# apparently its an array, so no need to process it's values as csv
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
if len(values_list) > 0:
ret = [x for x in values_list if x]
else:
ret = []
return list(set(ret))

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

155
docs/Makefile Normal file
View File

@ -0,0 +1,155 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-filter.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-filter.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-filter"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-filter"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
livehtml:
sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html

BIN
docs/assets/form.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

254
docs/conf.py Normal file
View File

@ -0,0 +1,254 @@
# -*- coding: utf-8 -*-
#
# django-filter documentation build configuration file, created by
# sphinx-quickstart on Mon Sep 17 11:25:20 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.txt'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'django-filter'
copyright = u'2013, Alex Gaynor and others.'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '1.1.0'
# The full version, including alpha/beta/rc tags.
release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
#html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'django-filterdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'django-filter.tex', u'django-filter Documentation',
u'Alex Gaynor and others.', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'django-filter', u'django-filter Documentation',
[u'Alex Gaynor and others.'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'django-filter', u'django-filter Documentation',
u'Alex Gaynor and others.', 'django-filter', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# see:
# https://github.com/snide/sphinx_rtd_theme#using-this-theme-locally-then-building-on-read-the-docs
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# only import and set the theme if we're building docs locally
if not on_rtd:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

90
docs/dev/tests.txt Normal file
View File

@ -0,0 +1,90 @@
======================
Running the Test Suite
======================
The easiest way to run the django-filter tests is to check out the source
code and create a virtualenv where you can install the test dependencies.
Django-filter uses a custom test runner to configure the environment, so a
wrapper script is available to set up and run the test suite.
.. note::
The following assumes you have `virtualenv`__ and `git`__ installed.
__ https://virtualenv.pypa.io/en/stable/
__ https://git-scm.com
Clone the repository
--------------------
Get the source code using the following command:
.. code-block:: bash
$ git clone https://github.com/carltongibson/django-filter.git
Switch to the django-filter directory:
.. code-block:: bash
$ cd django-filter
Set up the virtualenv
---------------------
Create a new virtualenv to run the test suite in:
.. code-block:: bash
$ virtualenv venv
Then activate the virtualenv and install the test requirements:
.. code-block:: bash
$ source venv/bin/activate
$ pip install -r requirements/test.txt
Execute the test runner
-----------------------
Run the tests with the runner script:
.. code-block:: bash
$ python runtests.py
Test all supported versions
---------------------------
You can also use the excellent tox testing tool to run the tests against all
supported versions of Python and Django. Install tox, and then simply run:
.. code-block:: bash
$ pip install tox
$ tox
Housekeeping
------------
The ``isort`` utility is used to maintain module imports. You can either test
the module imports with the appropriate `tox` env, or with `isort` directly.
.. code-block:: bash
$ pip install tox
$ tox -e isort
# or
$ pip install isort
$ isort --check-only --recursive django_filters tests
To sort the imports, simply remove the ``--check-only`` option.
.. code-block:: bash
$ isort --recursive django_filters tests

34
docs/guide/install.txt Normal file
View File

@ -0,0 +1,34 @@
============
Installation
============
Django-filter can be installed from PyPI with tools like ``pip``:
.. code-block:: bash
$ pip install django-filter
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. code-block:: python
INSTALLED_APPS = [
...
'django_filters',
]
Requirements
------------
Django-filter is tested against all supported versions of Python and `Django`__,
as well as the latest version of Django REST Framework (`DRF`__).
__ https://www.djangoproject.com/download/
__ http://www.django-rest-framework.org/
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.10, 1.11
* **DRF**: 3.7

231
docs/guide/migration.txt Normal file
View File

@ -0,0 +1,231 @@
===============
Migration Guide
===============
----------------
Migrating to 2.0
----------------
Removal of the ``Meta.together`` option
---------------------------------------
The ``Meta.together`` has been deprecated in favor of userland implementations
that override the ``clean`` method of the ``Meta.form`` class. An example will
be provided in a "recipes" secion in future docs.
``Filter.name`` renamed to ``Filter.field_name``
------------------------------------------------
The filter ``name`` has been renamed to ``field_name`` as a way to disambiguate
the filter's attribute name on its FilterSet class from the ``field_name`` used
for filtering purposes.
FilterSet strictness has been removed
-------------------------------------
Strictness handling has been removed from the ``FilterSet`` and added to the
view layer. As a result, the ``FILTERS_STRICTNESS`` setting, ``Meta.strict``
option, and ``strict`` argument for the ``FilterSet`` initializer have all
been removed.
To alter strictness behavior, the appropriate view code should be overridden.
More details will be provided in future docs.
----------------
Migrating to 1.0
----------------
The 1.0 release of django-filter introduces several API changes and refinements
that break forwards compatibility. Below is a list of deprecations and
instructions on how to migrate to the 1.0 release. A forwards-compatible 0.15
release has also been created to help with migration. It is compatible with
both the existing and new APIs and will raise warnings for deprecated behavior.
Enabling warnings
-----------------
To view the deprecations, you may need to enable warnings within python. This
can be achieved with either the ``-W`` `flag`__, or with ``PYTHONWARNINGS``
`environment variable`__. For example, you could run your test suite like so:
.. code-block:: bash
$ python -W once manage.py test
The above would print all warnings once when they first occur. This is useful
to know what violations exist in your code (or occasionally in third party
code). However, it only prints the last line of the stack trace. You can use
the following to raise the full exception instead:
.. code-block:: bash
$ python -W error manage.py test
__ https://docs.python.org/3.6/using/cmdline.html#cmdoption-W
__ https://docs.python.org/3.6/using/cmdline.html#envvar-PYTHONWARNINGS
MethodFilter and Filter.action replaced by Filter.method
--------------------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/382
The functionality of ``MethodFilter`` and ``Filter.action`` has been merged
together and replaced by the ``Filter.method`` parameter. The ``method``
parameter takes either a callable or the name of a ``FilterSet`` method. The
signature now takes an additional ``name`` argument that is the name of the
model field to be filtered on.
Since ``method`` is now a parameter of all filters, inputs are validated and
cleaned by its ``field_class``. The function will receive the cleaned value
instead of the raw value.
.. code-block:: python
# 0.x
class UserFilter(FilterSet):
last_login = filters.MethodFilter()
def filter_last_login(self, qs, value):
# try to convert value to datetime, which may fail.
if value and looks_like_a_date(value):
value = datetime(value)
return qs.filter(last_login=value})
# 1.0
class UserFilter(FilterSet):
last_login = filters.CharFilter(method='filter_last_login')
def filter_last_login(self, qs, name, value):
return qs.filter(**{name: value})
QuerySet methods are no longer proxied
--------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/440
The ``__iter__()``, ``__len__()``, ``__getitem__()``, ``count()`` methods are
no longer proxied from the queryset. To fix this, call the methods on the
``.qs`` property itself.
.. code-block:: python
f = UserFilter(request.GET, queryset=User.objects.all())
# 0.x
for obj in f:
...
# 1.0
for obj in f.qs:
...
Filters no longer autogenerated when Meta.fields is not specified
-----------------------------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/450
FilterSets had an undocumented behavior of autogenerating filters for all
model fields when either ``Meta.fields`` was not specified or when set to
``None``. This can lead to potentially unsafe data or schema exposure and
has been deprecated in favor of explicitly setting ``Meta.fields`` to the
``'__all__'`` special value. You may also blacklist fields by setting
the ``Meta.exclude`` attribute.
.. code-block:: python
class UserFilter(FilterSet):
class Meta:
model = User
fields = '__all__'
# or
class UserFilter(FilterSet):
class Meta:
model = User
exclude = ['password']
Move FilterSet options to Meta class
------------------------------------
Details: https://github.com/carltongibson/django-filter/issues/430
Several ``FilterSet`` options have been moved to the ``Meta`` class to prevent
potential conflicts with declared filter names. This includes:
* ``filter_overrides``
* ``strict``
* ``order_by_field``
.. code-block:: python
# 0.x
class UserFilter(FilterSet):
filter_overrides = {}
strict = STRICTNESS.RAISE_VALIDATION_ERROR
order_by_field = 'order'
...
# 1.0
class UserFilter(FilterSet):
...
class Meta:
filter_overrides = {}
strict = STRICTNESS.RAISE_VALIDATION_ERROR
order_by_field = 'order'
FilterSet ordering replaced by OrderingFilter
---------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/472
The FilterSet ordering options and methods have been deprecated and replaced
by :ref:`OrderingFilter <ordering-filter>`. Deprecated options include:
* ``Meta.order_by``
* ``Meta.order_by_field``
These options retain backwards compatibility with the following caveats:
* ``order_by`` asserts that ``Meta.fields`` is not using the dict syntax. This
previously was undefined behavior, however the migration code is unable to
support it.
* Prior, if no ordering was specified in the request, the FilterSet implicitly
filtered by the first param in the ``order_by`` option. This behavior cannot
be easily emulated but can be fixed by ensuring that the passed in queryset
explicitly calls ``.order_by()``.
.. code-block:: python
filterset = MyFilterSet(queryset=MyModel.objects.order_by('field'))
The following methods are deprecated and will raise an assertion if present
on the FilterSet:
* ``.get_order_by()``
* ``.get_ordering_field()``
To fix this, simply remove the methods from your class. You can subclass
``OrderingFilter`` to migrate any custom logic.
Deprecated ``FILTERS_HELP_TEXT_FILTER`` and ``FILTERS_HELP_TEXT_EXCLUDE``
-------------------------------------------------------------------------
Details: https://github.com/carltongibson/django-filter/pull/437
Generated filter labels in 1.0 will be more descriptive, including humanized
text about the lookup being performed and if the filter is an exclusion filter.
These settings will no longer have an effect and will be removed in the 1.0 release.
DRF filter backend raises ``TemplateDoesNotExist`` exception
------------------------------------------------------------
Templates are now provided by django-filter. If you are receiving this error,
you may need to add ``'django_filters'`` to your ``INSTALLED_APPS`` setting.
Alternatively, you could provide your own templates.

View File

@ -0,0 +1,189 @@
====================
Integration with DRF
====================
Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package.
__ http://www.django-rest-framework.org/
__ http://www.django-rest-framework.org/api-guide/filtering/
Quickstart
----------
Using the new ``FilterSet`` simply requires changing the import path. Instead of importing from ``django_filters``, import from the ``rest_framework`` sub-package.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
...
Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_backends``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('category', 'in_stock')
If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting.
.. code-block:: python
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
...
),
}
Adding a FilterSet with ``filter_class``
----------------------------------------
To enable filtering with a ``FilterSet``, add it to the ``filter_class`` parameter on your view class.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductFilter(filters.FilterSet):
min_price = filters.NumberFilter(name="price", lookup_expr='gte')
max_price = filters.NumberFilter(name="price", lookup_expr='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_class = ProductFilter
Using the ``filter_fields`` shortcut
------------------------------------
You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields <fields>`.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('category', 'in_stock')
# Equivalent FilterSet:
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
Schema Generation with Core API
-------------------------------
The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()``. This is automatically enabled when Core API is installed. Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``.
For example, your get queryset method may look like this:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_project(self):
return models.Project.objects.get(pk=self.kwargs['project_id'])
def get_queryset(self):
project = self.get_project()
return self.queryset \
.filter(project=project) \
.filter(author=self.request.user)
This could be rewritten like so:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_project(self):
try:
return models.Project.objects.get(pk=self.kwargs['project_id'])
except models.Project.DoesNotExist:
return None
def get_queryset(self):
project = self.get_project()
if project is None:
return self.queryset.none()
return self.queryset \
.filter(project=project) \
.filter(author=self.request.user)
Or more simply as:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_queryset(self):
# project_id may be None
return self.queryset \
.filter(project_id=self.kwargs.get('project_id')) \
.filter(author=self.request.user)
Crispy Forms
------------
If you are using DRF's browsable API or admin API you may also want to install ``django-crispy-forms``, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. Note that this isn't actively supported, although pull requests for bug fixes are welcome.
.. code-block:: bash
pip install django-crispy-forms
With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so:
.. image:: ../assets/form.png
Additional ``FilterSet`` Features
---------------------------------
The following features are specific to the rest framework FilterSet:
- ``BooleanFilter``'s use the API-friendly ``BooleanWidget``, which accepts lowercase ``true``/``false``.
- Filter generation uses ``IsoDateTimeFilter`` for datetime model fields.
- Raised ``ValidationError``'s are reraised as their DRF equivalent. This behavior is useful when setting FilterSet
strictness to ``STRICTNESS.RAISE_VALIDATION_ERROR``.

246
docs/guide/tips.txt Normal file
View File

@ -0,0 +1,246 @@
==================
Tips and Solutions
==================
Common problems for declared filters
------------------------------------
Below are some of the common problem that occur when declaring filters. It is
recommended that you read this as it provides a more complete understanding of
how filters work.
Filter ``name`` and ``lookup_expr`` not configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
them. By default, if ``name`` is not specified, the filter's name on the
filterset class will be used. Additionally, ``lookup_expr`` defaults to
``exact``. The following is an example of a misconfigured price filter:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter()
The filter instance will have a field name of ``price__gt`` and an ``exact``
lookup type. Under the hood, this will incorrectly be resolved as:
.. code-block:: python
Produce.objects.filter(price__gt__exact=value)
The above will most likely generate a ``FieldError``. The correct configuration
would be:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
Missing ``lookup_expr`` for text search filters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's quite common to forget to set the lookup expression for :code:`CharField`
and :code:`TextField` and wonder why a search for "foo" does not return results
for "foobar". This is because the default lookup type is ``exact``, but you
probably want to perform an ``icontains`` lookup.
Filter and lookup expression mismatch (in, range, isnull)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's not always appropriate to directly match a filter to its model field's
type, as some lookups expect different types of values. This is a commonly
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
at the following product model:
.. code-block:: python
class Product(models.Model):
category = models.ForeignKey(Category, null=True)
Given that ``category`` is optional, it's reasonable to want to enable a search
for uncategorized products. The following is an incorrectly configured
``isnull`` filter:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
So what's the issue? While the underlying column type for ``category`` is an
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
only validates numbers. Filters are not `'expression aware'` and won't change
behavior based on their ``lookup_expr``. You should use filters that match the
data type of the lookup expression `instead` of the data type underlying the
model field. The following would correctly allow you to search for both
uncategorized products and products for a set of categories:
.. code-block:: python
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
pass
class ProductFilter(django_filters.FilterSet):
categories = NumberInFilter(name='category', lookup_expr='in')
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
More info on constructing ``in`` and ``range`` csv :ref:`filters <base-in-filter>`.
Filtering by empty values
-------------------------
There are a number of cases where you may need to filter by empty or null
values. The following are some common solutions to these problems:
Filtering by null values
~~~~~~~~~~~~~~~~~~~~~~~~
As explained in the above "Filter and lookup expression mismatch" section, a
common problem is how to correctly filter by null values on a field.
Solution 1: Using a ``BooleanFilter`` with ``isnull``
"""""""""""""""""""""""""""""""""""""""""""""""""""""
Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by
the FilterSet's automatic filter generation. To do this manually, simply add:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
.. note::
Remember that the filter class is validating the input value. The underlying
type of the mode field is not relevant here.
You may also reverse the logic with the ``exclude`` parameter.
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True)
Solution 2: Using ``ChoiceFilter``'s null choice
""""""""""""""""""""""""""""""""""""""""""""""""
If you're using a ChoiceFilter, you may also filter by null values by enabling
the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
:ref:`docs <choice-filter>`.
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
category = django_filters.ModelChoiceFilter(
name='category', lookup_expr='isnull',
null_label='Uncategorized',
queryset=Category.objects.all(),
)
Solution 3: Combining fields w/ ``MultiValueField``
"""""""""""""""""""""""""""""""""""""""""""""""""""
An alternative approach is to use Django's ``MultiValueField`` to manually add
in a ``BooleanField`` to handle null values. Proof of concept:
https://github.com/carltongibson/django-filter/issues/446
Filtering by an empty string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's not currently possible to filter by an empty string, since empty values are
interpreted as a skipped filter.
GET http://localhost/api/my-model?myfield=
Solution 1: Magic values
""""""""""""""""""""""""
You can override the ``filter()`` method of a filter class to specifically check
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
GET http://localhost/api/my-model?myfield=EMPTY
.. code-block:: python
class MyCharFilter(filters.CharFilter):
empty_value = 'EMPTY'
def filter(self, qs, value):
if value != self.empty_value:
return super(MyCharFilter, self).filter(qs, value)
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""})
return qs.distinct() if self.distinct else qs
Solution 2: Empty string filter
"""""""""""""""""""""""""""""""
It would also be possible to create an empty value filter that exhibits the same
behavior as an ``isnull`` filter.
GET http://localhost/api/my-model?myfield__isempty=false
.. code-block:: python
from django.core.validators import EMPTY_VALUES
class EmptyStringFilter(filters.BooleanFilter):
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
exclude = self.exclude ^ (value is False)
method = qs.exclude if exclude else qs.filter
return method(**{self.name: ""})
class MyFilterSet(filters.FilterSet):
myfield__isempty = EmptyStringFilter(name='myfield')
class Meta:
model = MyModel
Using ``initial`` values as defaults
------------------------------------
In pre-1.0 versions of django-filter, a filter field's ``initial`` value was used as a
default when no value was submitted. This behavior was not officially supported and has
since been removed.
.. warning:: It is recommended that you do **NOT** implement the below as it adversely
affects usability. Django forms don't provide this behavior for a reason.
- Using initial values as defaults is inconsistent with the behavior of Django forms.
- Default values prevent users from filtering by empty values.
- Default values prevent users from skipping that filter.
If defaults are necessary though, the following should mimic the pre-1.0 behavior:
.. code-block:: python
class BaseFilterSet(FilterSet):
def __init__(self, data=None, *args, **kwargs):
# if filterset is bound, use initial values as defaults
if data is not None:
# get a mutable copy of the QueryDict
data = data.copy()
for name, f in self.base_filters.items():
initial = f.extra.get('initial')
# filter param is either missing or empty, use initial as default
if not data.get(name) and initial:
data[name] = initial
super(BaseFilterSet, self).__init__(data, *args, **kwargs)

337
docs/guide/usage.txt Normal file
View File

@ -0,0 +1,337 @@
===============
Getting Started
===============
Django-filter provides a simple way to filter down a queryset based on
parameters a user provides. Say we have a ``Product`` model and we want to let
our users filter which products they see on a list page.
.. note::
If you're using django-filter with Django Rest Framework, it's
recommended that you read the integration docs after this guide.
The model
---------
Let's start with our model::
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField()
description = models.TextField()
release_date = models.DateField()
manufacturer = models.ForeignKey(Manufacturer)
The filter
----------
We have a number of fields and we want to let our users filter based on the
name, the price or the release_date. We create a ``FilterSet`` for this::
import django_filters
class ProductFilter(django_filters.FilterSet):
name = django_filters.CharFilter(lookup_expr='iexact')
class Meta:
model = Product
fields = ['price', 'release_date']
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
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
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
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['price', 'release_date']
The above generates 'exact' lookups for both the 'price' and 'release_date'
fields.
Additionally, a dictionary can be used to specify multiple lookup expressions
for each field::
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'price': ['lt', 'gt'],
'release_date': ['exact', 'year__gt'],
}
The above would generate 'price__lt', 'price__gt', 'release_date', and
'release_date__year__gt' filters.
.. 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
"relationship paths" using Django's ``__`` syntax to filter on fields on a
related model::
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['manufacturer__country']
Overriding default filters
""""""""""""""""""""""""""
Like ``django.contrib.admin.ModelAdmin``, it is possible to override
default filters for all the models fields of the same kind using
``filter_overrides`` on the ``Meta`` class::
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'name': ['exact'],
'release_date': ['isnull'],
}
filter_overrides = {
models.CharField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains',
},
},
models.BooleanField: {
'filter_class': django_filters.BooleanFilter,
'extra': lambda f: {
'widget': forms.CheckboxInput,
},
},
}
Request-based filtering
~~~~~~~~~~~~~~~~~~~~~~~
The ``FilterSet`` may be initialized with an optional ``request`` argument. If
a request object is passed, then you may access the request during filtering.
This allows you to filter by properties on the request, such as the currently
logged-in user or the ``Accepts-Languages`` header.
.. note::
It is not guaranteed that a `request` will be provied to the `FilterSet`
instance. Any code depending on a request should handle the `None` case.
Filtering the primary ``.qs``
"""""""""""""""""""""""""""""
To filter the primary queryset by the ``request`` object, simply override the
``FilterSet.qs`` property. For example, you could filter blog articles to only
those that are published and those that are owned by the logged-in user
(presumably the author's draft articles).
.. code-block:: python
class ArticleFilter(django_filters.FilterSet):
class Meta:
model = Article
fields = [...]
@property
def qs(self):
parent = super(ArticleFilter, self).qs
author = getattr(self.request, 'user', None)
return parent.filter(is_published=True) \
| parent.filter(author=author)
Filtering the related queryset for ``ModelChoiceFilter``
""""""""""""""""""""""""""""""""""""""""""""""""""""""""
The ``queryset`` argument for ``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter``
supports callable behavior. If a callable is passed, it will be invoked with the
``request`` as its only argument. This allows you to perform the same kinds of
request-based filtering without resorting to overriding ``FilterSet.__init__``.
.. code-block:: python
def departments(request):
if request is None:
return Department.objects.none()
company = request.user.company
return company.department_set.all()
class EmployeeFilter(filters.FilterSet):
department = filters.ModelChoiceFilter(queryset=departments)
...
Customize filtering with ``Filter.method``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can control the behavior of a filter by specifying a ``method`` to perform
filtering. View more information in the :ref:`method reference <filter-method>`.
Note that you may access the filterset's properties, such as the ``request``.
.. code-block:: python
class F(django_filters.FilterSet):
username = CharFilter(method='my_custom_filter')
class Meta:
model = User
fields = ['username']
def my_custom_filter(self, queryset, name, value):
return queryset.filter(**{
name: value,
})
The view
--------
Now we need to write a view::
def product_list(request):
f = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': f})
If a queryset argument isn't provided then all the items in the default manager
of the model will be used.
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
The URL conf
------------
We need a URL pattern to call the view::
url(r'^list$', views.product_list)
The template
------------
And lastly we need a template::
{% extends "base.html" %}
{% block content %}
<form action="" method="get">
{{ filter.form.as_p }}
<input type="submit" />
</form>
{% for obj in filter.qs %}
{{ obj.name }} - ${{ obj.price }}<br />
{% endfor %}
{% endblock %}
And that's all there is to it! The ``form`` attribute contains a normal
Django form, and when we iterate over the ``FilterSet.qs`` we get the objects in
the resulting queryset.
Generic view & configuration
-----------------------------
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``.
You must provide either a ``model`` or ``filterset_class`` argument, similar to
``ListView`` in Django itself::
# urls.py
from django.conf.urls import url
from django_filters.views import FilterView
from myapp.models import Product
urlpatterns = [
url(r'^list/$', FilterView.as_view(model=Product)),
]
If you provide a ``model`` optionally you can set ``filter_fields`` to specify a list or a tuple of
the fields that you want to include for the automatic construction of the filterset class.
You must provide a template at ``<app>/<model>_filter.html`` which gets the
context parameter ``filter``. Additionally, the context will contain
``object_list`` which holds the filtered queryset.
A legacy functional generic view is still included in django-filter, although
its use is deprecated. It can be found at
``django_filters.views.object_filter``. You must provide the same arguments
to it as the class based view::
# urls.py
from django.conf.urls import url
from django_filters.views import object_filter
from myapp.models import Product
urlpatterns = [
url(r'^list/$', object_filter, {'model': Product}),
]
The needed template and its context variables will also be the same as the
class-based view above.

34
docs/index.txt Normal file
View File

@ -0,0 +1,34 @@
=============
django-filter
=============
Django-filter is a generic, reusable application to alleviate writing some of
the more mundane bits of view code. Specifically, it allows users to filter
down a queryset based on a model's fields, displaying the form to let them
do this.
.. toctree::
:maxdepth: 2
:caption: User Guide
guide/install
guide/usage
guide/rest_framework
guide/tips
guide/migration
.. toctree::
:maxdepth: 1
:caption: Reference Documentation
ref/filterset
ref/filters
ref/fields
ref/widgets
ref/settings
.. toctree::
:maxdepth: 1
:caption: Developer Documentation
dev/tests

190
docs/make.bat Normal file
View File

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-filter.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-filter.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

23
docs/ref/fields.txt Normal file
View File

@ -0,0 +1,23 @@
===============
Field Reference
===============
``IsoDateTimeField``
~~~~~~~~~~~~~~~~~~~~
Extends ``django.forms.DateTimeField`` to allow parsing ISO 8601 formated dates, in addition to existing formats
Defines a class level attribute ``ISO_8601`` as constant for the format.
Sets ``input_formats = [ISO_8601]`` — this means that by default ``IsoDateTimeField`` will **only** parse ISO 8601 formated dates.
You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format.
.. code-block:: python
f = IsoDateTimeField()
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

788
docs/ref/filters.txt Normal file
View File

@ -0,0 +1,788 @@
================
Filter Reference
================
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. If a label is not provided, a verbose label will be generated based
on the field ``name`` and the parts of the ``lookup_expr``.
(See: :ref:`verbose-lookups-setting`).
``widget``
~~~~~~~~~~
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.
* :ref:`RangeWidget <range-widget>` -- this widget is used with ``RangeFilter``
to generate two form input elements using a single field.
.. _filter-method:
``method``
~~~~~~~~~~
An optional argument that tells the filter how to handle the queryset. It can
accept either a callable or the name of a method on the ``FilterSet``. The
method receives a ``QuerySet``, the name of the model field to filter on, and
the value to filter with. It should return a ``Queryset`` that is filtered
appropriately.
The passed in value is validated and cleaned by the filter's ``field_class``,
so raw value transformation and empty value checking should be unnecessary.
.. code-block:: python
class F(FilterSet):
"""Filter for Books by if books are published or not"""
published = BooleanFilter(name='published_on', method='filter_published')
def filter_published(self, queryset, name, value):
# construct the full lookup expression.
lookup = '__'.join([name, 'isnull'])
return queryset.filter(**{lookup: False})
# alternatively, it may not be necessary to construct the lookup.
return queryset.filter(published_on__isnull=False)
class Meta:
model = Book
fields = ['published']
# Callables may also be defined out of the class scope.
def filter_not_empty(queryset, name, value):
lookup = '__'.join([name, 'isnull'])
return queryset.filter(**{lookup: False})
class F(FilterSet):
"""Filter for Books by if books are published or not"""
published = BooleanFilter(name='published_on', method=filter_not_empty)
class Meta:
model = Book
fields = ['published']
``lookup_expr``
~~~~~~~~~~~~~~~
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 ModelChoiceFilter and
ModelMultipleChoiceFilter only.
``queryset``
~~~~~~~~~~~~
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to
operate on which must be passed as a kwarg.
``to_field_name``
~~~~~~~~~~~~~~~~~
If you pass in ``to_field_name`` (which gets forwarded to the Django field),
it will be used also in the default ``get_filter_predicate`` implementation
as the model's attribute.
Filters
-------
``CharFilter``
~~~~~~~~~~~~~~
This filter does simple character matches, used with ``CharField`` and
``TextField`` by default.
``UUIDFilter``
~~~~~~~~~~~~~~
This filter matches UUID values, used with ``models.UUIDField`` by default.
``BooleanFilter``
~~~~~~~~~~~~~~~~~
This filter matches a boolean, either ``True`` or ``False``, used with
``BooleanField`` and ``NullBooleanField`` by default.
.. _choice-filter:
``ChoiceFilter``
~~~~~~~~~~~~~~~~
This filter matches values in its ``choices`` argument. The ``choices`` must be
explicitly passed when the filter is declared on the ``FilterSet``. For example,
.. code-block:: python
class User(models.Model):
username = models.CharField(max_length=255)
first_name = SubCharField(max_length=100)
last_name = SubSubCharField(max_length=100)
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
STATUS_CHOICES = (
(0, 'Regular'),
(1, 'Manager'),
(2, 'Admin'),
)
class F(FilterSet):
status = ChoiceFilter(choices=STATUS_CHOICES)
class Meta:
model = User
fields = ['status']
``ChoiceFilter`` also has arguments that enable a choice for not filtering, as
well as a choice for filtering by ``None`` values. Each of the arguments have a
corresponding global setting (:doc:`/ref/settings`).
* ``empty_label``: The display label to use for the select choice to not filter.
The choice may be disabled by setting this argument to ``None``. Defaults to
``FILTERS_EMPTY_CHOICE_LABEL``.
* ``null_label``: The display label to use for the choice to filter by ``None``
values. The choice may be disabled by setting this argument to ``None``.
Defaults to ``FILTERS_NULL_CHOICE_LABEL``.
* ``null_value``: The special value to match to enable filtering by ``None``
values. This value defaults ``FILTERS_NULL_CHOICE_VALUE`` and needs to be
a non-empty value (``''``, ``None``, ``[]``, ``()``, ``{}``).
``TypedChoiceFilter``
~~~~~~~~~~~~~~~~~~~~~
The same as ``ChoiceFilter`` with the added possibility to convert value to
match against. This could be done by using `coerce` parameter.
An example use-case is limiting boolean choices to match against so only
some predefined strings could be used as input of a boolean filter::
import django_filters
from distutils.util import strtobool
BOOLEAN_CHOICES = (('false', 'False'), ('true', 'True'),)
class YourFilterSet(django_filters.FilterSet):
...
flag = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES,
coerce=strtobool)
``MultipleChoiceFilter``
~~~~~~~~~~~~~~~~~~~~~~~~
The same as ``ChoiceFilter`` except the user can select multiple choices
and the filter will form the OR of these choices by default to match items.
The filter will form the AND of the selected choices when the ``conjoined=True``
argument is passed to this class.
Multiple choices are represented in the query string by reusing the same key with
different values (e.g. ''?status=Regular&status=Admin'').
``distinct`` defaults to ``True`` as to-many relationships will generally require this.
Advanced Use: Depending on your application logic, when all or no choices are
selected, filtering may be a noop. In this case you may wish to avoid the
filtering overhead, particularly of the `distinct` call.
Set `always_filter` to False after instantiation to enable the default `is_noop`
test.
Override `is_noop` if you require a different test for your application.
``TypedMultipleChoiceFilter``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Like ``MultipleChoiceFilter``, but in addition accepts the ``coerce`` parameter, as
in ``TypedChoiceFilter``.
``DateFilter``
~~~~~~~~~~~~~~
Matches on a date. Used with ``DateField`` by default.
``TimeFilter``
~~~~~~~~~~~~~~
Matches on a time. Used with ``TimeField`` by default.
``DateTimeFilter``
~~~~~~~~~~~~~~~~~~
Matches on a date and time. Used with ``DateTimeField`` by default.
``IsoDateTimeFilter``
~~~~~~~~~~~~~~~~~~~~~
Uses ``IsoDateTimeField`` to support filtering on ISO 8601 formatted dates, as are often
used in APIs, and are employed by default by Django REST Framework.
Example::
class F(FilterSet):
"""Filter for Books by date published, using ISO 8601 formatted dates"""
published = IsoDateTimeFilter()
class Meta:
model = Book
fields = ['published']
``DurationFilter``
~~~~~~~~~~~~~~~~~~
Matches on a duration. Used with ``DurationField`` by default.
Supports both Django ('%d %H:%M:%S.%f') and ISO 8601 formatted durations (but
only the sections that are accepted by Python's timedelta, so no year, month,
and week designators, e.g. 'P3DT10H22M').
``ModelChoiceFilter``
~~~~~~~~~~~~~~~~~~~~~
Similar to a ``ChoiceFilter`` except it works with related models, used for
``ForeignKey`` by default.
If automatically instantiated, ``ModelChoiceFilter`` will use the default
``QuerySet`` for the related field. If manually instantiated you **must**
provide the ``queryset`` kwarg.
Example::
class F(FilterSet):
"""Filter for books by author"""
author = ModelChoiceFilter(queryset=Author.objects.all())
class Meta:
model = Book
fields = ['author']
The ``queryset`` argument also supports callable behavior. If a callable is
passed, it will be invoked with ``Filterset.request`` as its only argument.
This allows you to easily filter by properties on the request object without
having to override the ``FilterSet.__init__``.
.. note::
You should expect that the `request` object may be `None`.
.. code-block:: python
def departments(request):
if request is None:
return Department.objects.none()
company = request.user.company
return company.department_set.all()
class EmployeeFilter(filters.FilterSet):
department = filters.ModelChoiceFilter(queryset=departments)
...
``ModelMultipleChoiceFilter``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Similar to a ``MultipleChoiceFilter`` except it works with related models, used
for ``ManyToManyField`` by default.
As with ``ModelChoiceFilter``, if automatically instantiated,
``ModelMultipleChoiceFilter`` will use the default ``QuerySet`` for the related
field. If manually instantiated you **must** provide the ``queryset`` kwarg.
Like ``ModelChoiceFilter``, the ``queryset`` argument has callable behavior.
To use a custom field name for the lookup, you can use ``to_field_name``::
class FooFilter(BaseFilterSet):
foo = django_filters.filters.ModelMultipleChoiceFilter(
name='attr__uuid',
to_field_name='uuid',
queryset=Foo.objects.all(),
)
If you want to use a custom queryset, e.g. to add annotated fields, this can be
done as follows::
class MyMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
def get_filter_predicate(self, v):
return {'annotated_field': v.annotated_field}
def filter(self, qs, value):
if value:
qs = qs.annotate_with_custom_field()
qs = super().filter(qs, value)
return qs
foo = MyMultipleChoiceFilter(
to_field_name='annotated_field',
queryset=Model.objects.annotate_with_custom_field(),
)
The ``annotate_with_custom_field`` method would be defined through a custom
QuerySet, which then gets used as the model's manager::
class CustomQuerySet(models.QuerySet):
def annotate_with_custom_field(self):
return self.annotate(
custom_field=Case(
When(foo__isnull=False,
then=F('foo__uuid')),
When(bar__isnull=False,
then=F('bar__uuid')),
default=None,
),
)
class MyModel(models.Model):
objects = CustomQuerySet.as_manager()
``NumberFilter``
~~~~~~~~~~~~~~~~
Filters based on a numerical value, used with ``IntegerField``, ``FloatField``,
and ``DecimalField`` by default.
``NumericRangeFilter``
~~~~~~~~~~~~~~~~~~~~~~
Filters where a value is between two numerical values, or greater than a minimum or less
than a maximum where only one limit value is provided. This filter is designed to work
with the Postgres Numerical Range Fields, including ``IntegerRangeField``,
``BigIntegerRangeField`` and ``FloatRangeField`` (available since Django 1.8). The default
widget used is the ``RangeField``.
Regular field lookups are available in addition to several containment lookups, including
``overlap``, ``contains``, and ``contained_by``. More details in the Django `docs`__.
__ https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields
If the lower limit value is provided, the filter automatically defaults to ``startswith``
as the lookup and ``endswith`` if only the upper limit value is provided.
``RangeFilter``
~~~~~~~~~~~~~~~
Filters where a value is between two numerical values, or greater than a minimum or less than a maximum where only one limit value is provided. ::
class F(FilterSet):
"""Filter for Books by Price"""
price = RangeFilter()
class Meta:
model = Book
fields = ['price']
qs = Book.objects.all().order_by('title')
# Range: Books between 5€ and 15€
f = F({'price_0': '5', 'price_1': '15'}, queryset=qs)
# Min-Only: Books costing more the 11€
f = F({'price_0': '11'}, queryset=qs)
# Max-Only: Books costing less than 19€
f = F({'price_1': '19'}, queryset=qs)
``DateRangeFilter``
~~~~~~~~~~~~~~~~~~~
Filter similar to the admin changelist date one, it has a number of common
selections for working with date fields.
``DateFromToRangeFilter``
~~~~~~~~~~~~~~~~~~~~~~~~~
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'})
.. note::
When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime.
This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it.
.. warning::
If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively.
This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware.
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``
~~~~~~~~~~~~~~~~~~~
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``
~~~~~~~~~~~~~~~~~~~
This is a ``ChoiceFilter`` whose choices are the current values in the
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
of the admin.
``AllValuesMultipleFilter``
~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is a ``MultipleChoiceFilter`` whose choices are the current values in the
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
of the admin.
.. _base-in-filter:
``BaseInFilter``
~~~~~~~~~~~~~~~~
This is a base class used for creating IN lookup filters. It is expected that
this filter class is used in conjunction with another filter class, as this
class **only** validates that the incoming value is comma-separated. The secondary
filter is then used to validate the individual values.
Example::
class NumberInFilter(BaseInFilter, NumberFilter):
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 creating RANGE lookup filters. It behaves
identically to ``BaseInFilter`` with the exception that it expects only two
comma-separated values.
Example::
class NumberRangeFilter(BaseInFilter, NumberFilter):
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
.. _ordering-filter:
``OrderingFilter``
~~~~~~~~~~~~~~~~~~
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
two additional arguments that are used to build the ordering choices.
* ``fields`` is a mapping of {model field name: parameter name}. The
parameter names are exposed in the choices and mask/alias the field
names used in the ``order_by()`` call. Similar to field ``choices``,
``fields`` accepts the 'list of two-tuples' syntax that retains order.
``fields`` may also just be an iterable of strings. In this case, the
field names simply double as the exposed parameter names.
* ``field_labels`` is an optional argument that allows you to customize
the display label for the corresponding parameter. It accepts a mapping
of {field name: human readable label}. Keep in mind that the key is the
field name, and not the exposed parameter name.
.. code-block:: python
class UserFilter(FilterSet):
account = CharFilter(name='username')
status = NumberFilter(name='status')
o = OrderingFilter(
# tuple-mapping retains order
fields=(
('username', 'account'),
('first_name', 'first_name'),
('last_name', 'last_name'),
),
# labels do not need to retain order
field_labels={
'username': 'User account',
}
)
class Meta:
model = User
fields = ['first_name', 'last_name']
>>> UserFilter().filters['o'].field.choices
[
('account', 'User account'),
('-account', 'User account (descending)'),
('first_name', 'First name'),
('-first_name', 'First name (descending)'),
('last_name', 'Last name'),
('-last_name', 'Last name (descending)'),
]
Additionally, you can just provide your own ``choices`` if you require
explicit control over the exposed options. For example, when you might
want to disable descending sort options.
.. code-block:: python
class UserFilter(FilterSet):
account = CharFilter(name='username')
status = NumberFilter(name='status')
o = OrderingFilter(
choices=(
('account', 'Account'),
),
fields={
'username': 'account',
},
)
This filter is also CSV-based, and accepts multiple ordering params. The
default select widget does not enable the use of this, but it is useful
for APIs. ``SelectMultiple`` widgets are not compatible, given that they
are not able to retain selection order.
Adding Custom filter choices
""""""""""""""""""""""""""""
If you wish to sort by non-model fields, you'll need to add custom handling to an
``OrderingFilter`` subclass. For example, if you want to sort by a computed
'relevance' factor, you would need to do something like the following:
.. code-block:: python
class CustomOrderingFilter(django_filters.OrderingFilter):
def __init__(self, *args, **kwargs):
super(CustomOrderingFilter, self).__init__(*args, **kwargs)
self.extra['choices'] += [
('relevance', 'Relevance'),
('-relevance', 'Relevance (descending)'),
]
def filter(self, qs, value):
# OrderingFilter is CSV-based, so `value` is a list
if any(v in ['relevance', '-relevance'] for v in value):
# sort queryset by relevance
return ...
return super(CustomOrderingFilter, self).filter(qs, value)

215
docs/ref/filterset.txt Normal file
View File

@ -0,0 +1,215 @@
=================
FilterSet Options
=================
This document provides a guide on using additional FilterSet features.
Meta options
------------
- :ref:`model <model>`
- :ref:`fields <fields>`
- :ref:`exclude <exclude>`
- :ref:`form <form>`
- :ref:`together <together>`
- :ref:`filter_overrides <filter_overrides>`
- :ref:`strict <strict>`
.. _model:
Automatic filter generation with ``model``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``FilterSet`` is capable of automatically generating filters for a given
``model``'s fields. Similar to Django's ``ModelForm``, filters are created
based on the underlying model field's type. This option must be combined with
either the ``fields`` or ``exclude`` option, which is the same requirement for
Django's ``ModelForm`` class, detailed `here`__.
__ https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#selecting-the-fields-to-use
.. code-block:: python
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = ['username', 'last_login']
.. _fields:
Declaring filterable ``fields``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``fields`` option is combined with ``model`` to automatically generate
filters. Note that generated filters will not overwrite filters declared on
the ``FilterSet``. The ``fields`` option accepts two syntaxes:
* a list of field names
* a dictionary of field names mapped to a list of lookups
.. code-block:: python
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = ['username', 'last_login']
# or
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
fields = {
'username': ['exact', 'contains'],
'last_login': ['exact', 'year__gt'],
}
The list syntax will create an ``exact`` lookup filter for each field included
in ``fields``. The dictionary syntax will create a filter for each lookup
expression declared for its corresponding model field. These expressions may
include both transforms and lookups, as detailed in the `lookup reference`__.
__ https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups
.. _exclude:
Disable filter fields with ``exclude``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``exclude`` option accepts a blacklist of field names to exclude from
automatic filter generation. Note that this option will not disable filters
declared directly on the ``FilterSet``.
.. code-block:: python
class UserFilter(django_filters.FilterSet):
class Meta:
model = User
exclude = ['password']
.. _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']
.. _filter_overrides:
Customise filter generation with ``filter_overrides``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The inner ``Meta`` class also takes an optional ``filter_overrides`` argument.
This is a map of model fields to filter classes with options::
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['name', 'release_date']
filter_overrides = {
models.CharField: {
'filter_class': django_filters.CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains',
},
},
models.BooleanField: {
'filter_class': django_filters.BooleanFilter,
'extra': lambda f: {
'widget': forms.CheckboxInput,
},
},
}
.. _strict:
Handling validation errors with ``strict``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``strict`` option determines the filterset's behavior when filters fail to validate. Example use:
.. code-block:: python
from django_filters import FilterSet, STRICTNESS
class ProductFilter(FilterSet):
class Meta:
model = Product
fields = ['name', 'release_date']
strict = STRICTNESS.RETURN_NO_RESULTS
Currently, there are three different behaviors:
- ``STRICTNESS.RETURN_NO_RESULTS`` (default) This returns an empty queryset. The
filterset form can then be rendered to display the input errors.
- ``STRICTNESS.IGNORE`` Instead of returning an empty queryset, invalid filters
effectively become a noop. Valid filters are applied to the queryset however.
- ``STRICTNESS.RAISE_VALIDATION_ERROR`` This raises a ``ValidationError`` for
all invalid filters. This behavior is generally useful with APIs.
If the ``strict`` option is not provided, then the filterset will default to the
value of the ``FILTERS_STRICTNESS`` setting.
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.DateRangeFilter, {}
# use default behavior otherwise
return super(ProductFilter, cls).filter_for_lookup(f, lookup_type)

106
docs/ref/settings.txt Normal file
View File

@ -0,0 +1,106 @@
==================
Settings Reference
==================
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_EMPTY_CHOICE_LABEL
--------------------------
Default: ``'---------'``
Set the default value for ``ChoiceFilter.empty_label``. You may disable the empty choice by setting this to ``None``.
FILTERS_NULL_CHOICE_LABEL
-------------------------
Default: ``None``
Set the default value for ``ChoiceFilter.null_label``. You may enable the null choice by setting a non-``None`` value.
FILTERS_NULL_CHOICE_VALUE
-------------------------
Default: ``'null'``
Set the default value for ``ChoiceFilter.null_value``. You may want to change this value if the default ``'null'`` string conflicts with an actual choice.
FILTERS_DISABLE_HELP_TEXT
-------------------------
Default: ``False``
Some filters provide informational ``help_text``. For example, csv-based
filters (``filters.BaseCSVFilter``) inform users that "Multiple values may
be separated by commas".
You may set this to ``True`` to disable the ``help_text`` for **all**
filters, removing the text from the rendered form's output.
.. _verbose-lookups-setting:
FILTERS_VERBOSE_LOOKUPS
-----------------------
.. note::
This is considered an advanced setting and is subject to change.
Default:
.. code-block:: python
# refer to 'django_filters.conf.DEFAULTS'
'VERBOSE_LOOKUPS': {
'exact': _(''),
'iexact': _(''),
'contains': _('contains'),
'icontains': _('contains'),
...
}
This setting controls the verbose output for generated filter labels. Instead
of getting expression parts such as "lt" and "contained_by", the verbose label
would contain "is less than" and "is contained by". Verbose output may be
disabled by setting this to a falsy value.
This setting also accepts callables. The callable should not require arguments
and should return a dictionary. This is useful for extending or overriding the
default terms without having to copy the entire set of terms to your settings.
For example, you could add verbose output for "exact" lookups.
.. code-block:: python
# settings.py
def FILTERS_VERBOSE_LOOKUPS():
from django_filters.conf import DEFAULTS
verbose_lookups = DEFAULTS['VERBOSE_LOOKUPS'].copy()
verbose_lookups.update({
'exact': 'is equal to',
})
return verbose_lookups
FILTERS_STRICTNESS
------------------
Default: ``STRICTNESS.RETURN_NO_RESULTS``
Set the global default for FilterSet :ref:`strictness <strict>`. If ``strict`` is not
provided to the filterset, it will default to this setting. You can change the setting
like so:
.. code-block:: python
# settings.py
from django_filters import STRICTNESS
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS

88
docs/ref/widgets.txt Normal file
View File

@ -0,0 +1,88 @@
================
Widget Reference
================
This is a reference document with a list of the provided widgets and their
arguments.
.. _link-widget:
``LinkWidget``
~~~~~~~~~~~~~~
This widget renders each option as a link, instead of an actual <input>. It has
one method that you can override for additional customizability.
``option_string()`` should return a string with 3 Python keyword argument
placeholders:
1. ``attrs``: This is a string with all the attributes that will be on the
final ``<a>`` tag.
2. ``query_string``: This is the query string for use in the ``href``
option on the ``<a>`` element.
3. ``label``: This is the text to be displayed to the user.
.. _boolean-widget:
``BooleanWidget``
~~~~~~~~~~~~~~~~~
This widget converts its input into Python's True/False values. It will convert
all case variations of ``True`` and ``False`` into the internal Python values.
To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``:
.. code-block:: python
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.
.. _range-widget:
``RangeWidget``
~~~~~~~~~~~~~~~
This widget is used with ``RangeFilter`` and its subclasses. It generates two
form input elements which generally act as start/end values in a range.
Under the hood, it is django's ``forms.TextInput`` widget and excepts
the same arguments and values. To use it, pass it to ``widget`` argument of
a ``RangeField``:
.. code-block:: python
date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'}))
``SuffixedMultiWidget``
~~~~~~~~~~~~~~~~~~~~~~~
Extends Django's builtin ``MultiWidget`` to append custom suffixes instead of
indices. For example, take a range widget that accepts minimum and maximum
bounds. By default, the resulting query params would look like the following:
.. code-block:: http
GET /products?price_0=10&price_1=25 HTTP/1.1
By using ``SuffixedMultiWidget`` instead, you can provide human-friendly suffixes.
.. code-block:: python
class RangeWidget(SuffixedMultiWidget):
suffixes = ['min', 'max']
The query names are now a little more ergonomic.
.. code-block:: http
GET /products?price_min=10&price_max=25 HTTP/1.1

View File

@ -0,0 +1,27 @@
alabaster==0.7.7
argh==0.26.1
Babel==2.2.0
backports.ssl-match-hostname==3.4.0.2
bumpversion==0.5.3
certifi==2015.9.6.2
docutils==0.12
funcsigs==0.4
Jinja2==2.8
livereload==2.4.0
MarkupSafe==0.23
pathtools==0.1.2
pbr==1.7.0
pkginfo==1.2.1
Pygments==2.1.3
pytz==2016.6.1
PyYAML==3.11
requests==2.9.1
requests-toolbelt==0.6.0
six==1.9.0
snowballstemmer==1.2.1
Sphinx==1.3.6
sphinx-autobuild==0.6.0
sphinx-rtd-theme==0.1.9
tornado==4.2.1
twine==1.6.5
watchdog==0.8.3

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

@ -0,0 +1,7 @@
markdown==2.6.4
coreapi
django-crispy-forms
coverage
mock
pytz

3
requirements/test.txt Normal file
View File

@ -0,0 +1,3 @@
-r test-ci.txt
django
djangorestframework

19
runshell.py Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python
import os
import sys
import django
from django.core.management import execute_from_command_line
def runshell():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
execute_from_command_line(
sys.argv[:1] +
['migrate', '--noinput', '-v', '0'] +
(['--run-syncdb'] if django.VERSION >= (1, 9) else []))
argv = sys.argv[:1] + ['shell'] + sys.argv[1:]
execute_from_command_line(argv)
if __name__ == '__main__':
runshell()

14
runtests.py Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
import os
import sys
from django.core.management import execute_from_command_line
def runtests():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
argv = sys.argv[:1] + ['test'] + sys.argv[1:]
execute_from_command_line(argv)
if __name__ == '__main__':
runtests()

19
setup.cfg Normal file
View File

@ -0,0 +1,19 @@
[metadata]
license-file = LICENSE
[wheel]
universal = 1
[isort]
skip = .tox
atomic = true
multi_line_output = 3
known_standard_library = mock
known_third_party = django,pytz,rest_framework
known_first_party = django_filters
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0

60
setup.py Normal file
View File

@ -0,0 +1,60 @@
import os
import sys
from setuptools import setup, find_packages
f = open('README.rst')
readme = f.read()
f.close()
version = '1.1.0'
if sys.argv[-1] == 'publish':
if os.system("pip freeze | grep wheel"):
print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
sys.exit()
if os.system("pip freeze | grep twine"):
print("twine not installed.\nUse `pip install twine`.\nExiting.")
sys.exit()
os.system("python setup.py sdist bdist_wheel")
os.system("twine upload dist/*")
print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")
sys.exit()
setup(
name='django-filter',
version=version,
description=('Django-filter is a reusable Django application for allowing'
' users to filter querysets dynamically.'),
long_description=readme,
author='Alex Gaynor',
author_email='alex.gaynor@gmail.com',
maintainer='Carlton Gibson',
maintainer_email='carlton.gibson@noumenal.es',
url='https://github.com/carltongibson/django-filter/tree/master',
packages=find_packages(exclude=['tests*']),
include_package_data=True,
license='BSD',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
],
zip_safe=False,
)

0
tests/__init__.py Normal file
View File

214
tests/models.py Normal file
View File

@ -0,0 +1,214 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
REGULAR = 0
MANAGER = 1
ADMIN = 2
STATUS_CHOICES = (
(REGULAR, 'Regular'),
(MANAGER, 'Manager'),
(ADMIN, 'Admin'),
)
# classes for testing filters with inherited fields
class SubCharField(models.CharField):
pass
class SubSubCharField(SubCharField):
pass
class SubnetMaskField(models.Field):
empty_strings_allowed = False
description = "Subnet Mask"
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 15
models.Field.__init__(self, *args, **kwargs)
def get_internal_type(self):
return "GenericIPAddressField"
def formfield(self, **kwargs):
defaults = {'form_class': forms.GenericIPAddressField}
defaults.update(kwargs)
return super(SubnetMaskField, self).formfield(**defaults)
@python_2_unicode_compatible
class User(models.Model):
username = models.CharField(_('username'), max_length=255)
first_name = SubCharField(max_length=100)
last_name = SubSubCharField(max_length=100)
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
is_active = models.BooleanField(default=False)
favorite_books = models.ManyToManyField('Book', related_name='lovers')
def __str__(self):
return self.username
@python_2_unicode_compatible
class ManagerGroup(models.Model):
users = models.ManyToManyField(User,
limit_choices_to={'is_active': True},
related_name='member_of')
manager = models.ForeignKey(User,
limit_choices_to=lambda: {'status': MANAGER},
related_name='manager_of',
on_delete=models.CASCADE)
def __str__(self):
return self.manager.name + ' group'
@python_2_unicode_compatible
class AdminUser(User):
class Meta:
proxy = True
def __str__(self):
return "%s (ADMIN)" % self.username
@python_2_unicode_compatible
class Comment(models.Model):
text = models.TextField()
author = models.ForeignKey(User, related_name='comments', on_delete=models.CASCADE)
date = models.DateField()
time = models.TimeField()
def __str__(self):
return "%s said %s" % (self.author, self.text[:25])
class Article(models.Model):
name = models.CharField(verbose_name='title', max_length=200, blank=True)
published = models.DateTimeField()
author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
def __str__(self):
if self.author_id:
return "%s on %s" % (self.author, self.published)
return "Anonymous on %s" % self.published
@python_2_unicode_compatible
class Book(models.Model):
title = models.CharField(max_length=100)
price = models.DecimalField(max_digits=6, decimal_places=2)
average_rating = models.FloatField()
def __str__(self):
return self.title
class Place(models.Model):
name = models.CharField(max_length=100)
class Meta:
abstract = True
class Restaurant(Place):
serves_pizza = models.BooleanField(default=False)
class NetworkSetting(models.Model):
ip = models.GenericIPAddressField()
mask = SubnetMaskField()
cidr = models.CharField(max_length=18, blank=True, verbose_name="CIDR")
@python_2_unicode_compatible
class Company(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Meta:
ordering = ['name']
@python_2_unicode_compatible
class Location(models.Model):
company = models.ForeignKey(Company, related_name='locations', on_delete=models.CASCADE)
name = models.CharField(max_length=100)
zip_code = models.CharField(max_length=10)
open_days = models.CharField(max_length=7)
def __str__(self):
return '%s: %s' % (self.company.name, self.name)
class Account(models.Model):
name = models.CharField(max_length=100)
in_good_standing = models.BooleanField(default=False)
friendly = models.BooleanField(default=False)
class Profile(models.Model):
account = models.OneToOneField(Account, related_name='profile', on_delete=models.CASCADE)
likes_coffee = models.BooleanField(default=False)
likes_tea = models.BooleanField(default=False)
class BankAccount(Account):
amount_saved = models.IntegerField(default=0)
class Node(models.Model):
name = models.CharField(max_length=20)
adjacents = models.ManyToManyField('self')
class DirectedNode(models.Model):
name = models.CharField(max_length=20)
outbound_nodes = models.ManyToManyField('self',
symmetrical=False,
related_name='inbound_nodes')
class Worker(models.Model):
name = models.CharField(max_length=100)
class HiredWorker(models.Model):
salary = models.IntegerField()
hired_on = models.DateField()
worker = models.ForeignKey(Worker, on_delete=models.CASCADE)
business = models.ForeignKey('Business', on_delete=models.CASCADE)
class Business(models.Model):
name = models.CharField(max_length=100)
employees = models.ManyToManyField(Worker,
through=HiredWorker,
related_name='employers')
class UUIDTestModel(models.Model):
uuid = models.UUIDField()
class SpacewalkRecord(models.Model):
"""Cumulative space walk record.
See: https://en.wikipedia.org/wiki/List_of_cumulative_spacewalk_records
"""
astronaut = models.CharField(max_length=100)
duration = models.DurationField()

View File

@ -0,0 +1 @@
default_app_config = 'tests.rest_framework.apps.RestFrameworkTestConfig'

View File

@ -0,0 +1,8 @@
from django.apps import AppConfig
class RestFrameworkTestConfig(AppConfig):
name = 'tests.rest_framework'
label = 'drf_test_app'
verbose_name = "Rest Framework Test App"

View File

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

View File

@ -0,0 +1 @@
Test

View File

@ -0,0 +1,229 @@
from __future__ import unicode_literals
import datetime
import warnings
from decimal import Decimal
from unittest import skipIf
from django.db.models import BooleanField
from django.test import TestCase
from django.test.utils import override_settings
from rest_framework import generics, serializers
from rest_framework.test import APIRequestFactory
from django_filters import compat, filters
from django_filters.rest_framework import (
DjangoFilterBackend,
FilterSet,
backends
)
from .models import FilterableItem
factory = APIRequestFactory()
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
fields = '__all__'
# Basic filter on a list view.
class FilterFieldsRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date']
filter_backends = (DjangoFilterBackend,)
# These class are used to test a filter class.
class SeveralFieldsFilter(FilterSet):
text = filters.CharFilter(lookup_expr='icontains')
decimal = filters.NumberFilter(lookup_expr='lt')
date = filters.DateFilter(lookup_expr='gt')
class Meta:
model = FilterableItem
fields = ['text', 'decimal', 'date']
class FilterClassRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
@skipIf(compat.coreapi is None, 'coreapi must be installed')
class GetSchemaFieldsTests(TestCase):
def test_fields_with_filter_fields_list(self):
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(FilterFieldsRootView())
fields = [f.name for f in fields]
self.assertEqual(fields, ['decimal', 'date'])
def test_filter_fields_list_with_bad_get_queryset(self):
"""
See:
* https://github.com/carltongibson/django-filter/issues/551
"""
class BadGetQuerySetView(FilterFieldsRootView):
def get_queryset(self):
raise AttributeError("I don't have that")
backend = DjangoFilterBackend()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
fields = backend.get_schema_fields(BadGetQuerySetView())
self.assertEqual(fields, [], "get_schema_fields should handle AttributeError")
warning = "{} is not compatible with schema generation".format(BadGetQuerySetView)
self.assertEqual(len(w), 1)
self.assertEqual(str(w[0].message), warning)
def test_fields_with_filter_fields_dict(self):
class DictFilterFieldsRootView(FilterFieldsRootView):
filter_fields = {
'decimal': ['exact', 'lt', 'gt'],
}
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(DictFilterFieldsRootView())
fields = [f.name for f in fields]
self.assertEqual(fields, ['decimal', 'decimal__lt', 'decimal__gt'])
def test_fields_with_filter_class(self):
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(FilterClassRootView())
schemas = [f.schema for f in fields]
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date'])
self.assertIsInstance(schemas[0], compat.coreschema.String)
self.assertIsInstance(schemas[1], compat.coreschema.Number)
self.assertIsInstance(schemas[2], compat.coreschema.String)
def test_field_required(self):
class RequiredFieldsFilter(SeveralFieldsFilter):
required_text = filters.CharFilter(required=True)
class Meta(SeveralFieldsFilter.Meta):
fields = SeveralFieldsFilter.Meta.fields + ['required_text']
class FilterClassWithRequiredFieldsView(FilterClassRootView):
filter_class = RequiredFieldsFilter
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(FilterClassWithRequiredFieldsView())
required = [f.required for f in fields]
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date', 'required_text'])
self.assertFalse(required[0])
self.assertFalse(required[1])
self.assertFalse(required[2])
self.assertTrue(required[3])
def tests_field_with_request_callable(self):
def qs(request):
# users expect a valid request object to be provided which cannot
# be guaranteed during schema generation.
self.fail("callable queryset should not be invoked during schema generation")
class F(SeveralFieldsFilter):
f = filters.ModelChoiceFilter(queryset=qs)
class View(FilterClassRootView):
filter_class = F
view = View()
view.request = factory.get('/')
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(view)
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date', 'f'])
class TemplateTests(TestCase):
def test_backend_output(self):
"""
Ensure backend renders default if template path does not exist
"""
view = FilterFieldsRootView()
backend = view.filter_backends[0]
request = view.initialize_request(factory.get('/'))
html = backend().to_html(request, view.get_queryset(), view)
self.assertHTMLEqual(html, """
<h2>Field filters</h2>
<form class="form" action="" method="get">
<p>
<label for="id_decimal">Decimal:</label>
<input id="id_decimal" name="decimal" step="any" type="number" />
</p>
<p>
<label for="id_date">Date:</label>
<input id="id_date" name="date" type="text" />
</p>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
""")
def test_template_path(self):
view = FilterFieldsRootView()
class Backend(view.filter_backends[0]):
template = 'filter_template.html'
request = view.initialize_request(factory.get('/'))
html = Backend().to_html(request, view.get_queryset(), view)
self.assertHTMLEqual(html, "Test")
@override_settings(TEMPLATES=[])
def test_DTL_missing(self):
# The backend should be importable even if the DTL is not used.
# See: https://github.com/carltongibson/django-filter/issues/506
try:
from importlib import reload # python 3.4
except ImportError:
from imp import reload
reload(backends)
def test_multiple_engines(self):
# See: https://github.com/carltongibson/django-filter/issues/578
DTL = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True}
ALT = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'NAME': 'alt'}
# multiple DTL backends
with override_settings(TEMPLATES=[DTL, ALT]):
self.test_backend_output()
class DefaultFilterSetTests(TestCase):
def test_default_meta_inheritance(self):
# https://github.com/carltongibson/django-filter/issues/663
class F(FilterSet):
class Meta:
filter_overrides = {BooleanField: {}}
class Backend(DjangoFilterBackend):
default_filter_set = F
view = FilterFieldsRootView()
backend = Backend()
filter_class = backend.get_filter_class(view, view.get_queryset())
filter_overrides = filter_class._meta.filter_overrides
# derived filter_class.Meta should inherit from default_filter_set.Meta
self.assertIn(BooleanField, filter_overrides)
self.assertDictEqual(filter_overrides[BooleanField], {})

View File

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

View File

@ -0,0 +1,45 @@
from unittest import skipIf
from django.conf import settings
from django.test import TestCase
from django.test.utils import override_settings
from django_filters.compat import is_crispy
from django_filters.rest_framework import FilterSet, filters
from django_filters.widgets import BooleanWidget
from ..models import Article, User
class ArticleFilter(FilterSet):
class Meta:
model = Article
fields = ['author']
class FilterSetFilterForFieldTests(TestCase):
def test_isodatetimefilter(self):
field = Article._meta.get_field('published')
result = FilterSet.filter_for_field(field, 'published')
self.assertIsInstance(result, filters.IsoDateTimeFilter)
self.assertEqual(result.name, 'published')
def test_booleanfilter_widget(self):
field = User._meta.get_field('is_active')
result = FilterSet.filter_for_field(field, 'is_active')
self.assertIsInstance(result, filters.BooleanFilter)
self.assertEqual(result.extra['widget'], BooleanWidget)
@skipIf(is_crispy(), 'django_crispy_forms must be installed')
@override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ('crispy_forms', ))
class CrispyFormsCompatTests(TestCase):
def test_crispy_helper(self):
# ensure the helper is present on the form
self.assertTrue(hasattr(ArticleFilter().form, 'helper'))
def test_form_initialization(self):
# ensure that crispy compat does not prematurely initialize the form
self.assertFalse(hasattr(ArticleFilter(), '_form'))

View File

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

45
tests/settings.py Normal file
View File

@ -0,0 +1,45 @@
# ensure package/conf is importable
from django_filters import STRICTNESS
from django_filters.conf import DEFAULTS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
},
}
INSTALLED_APPS = (
'django.contrib.contenttypes',
'django.contrib.staticfiles',
'django.contrib.auth',
'rest_framework',
'django_filters',
'tests.rest_framework',
'tests',
)
MIDDLEWARE = []
ROOT_URLCONF = 'tests.urls'
USE_TZ = True
SECRET_KEY = 'foobar'
TEMPLATES = [{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
}]
STATIC_URL = '/static/'
# help verify that DEFAULTS is importable from conf.
def FILTERS_VERBOSE_LOOKUPS():
return DEFAULTS['VERBOSE_LOOKUPS']
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS

1339
tests/tags Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
{{ filter.form }}
{% for obj in filter.qs %}
{{ obj }}
{% endfor %}

141
tests/test_conf.py Normal file
View File

@ -0,0 +1,141 @@
from django.test import TestCase, override_settings
from django_filters import STRICTNESS, FilterSet
from django_filters.conf import is_callable, settings
from tests.models import User
class DefaultSettingsTests(TestCase):
def test_verbose_lookups(self):
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
self.assertIn('exact', settings.VERBOSE_LOOKUPS)
def test_disable_help_text(self):
self.assertFalse(settings.DISABLE_HELP_TEXT)
def test_strictness(self):
self.assertEqual(settings.STRICTNESS, STRICTNESS.RETURN_NO_RESULTS)
def test_help_text_filter(self):
self.assertTrue(settings.HELP_TEXT_FILTER)
def test_help_text_exclude(self):
self.assertTrue(settings.HELP_TEXT_EXCLUDE)
def test_empty_choice_label(self):
self.assertEqual(settings.EMPTY_CHOICE_LABEL, '---------')
def test_null_choice_label(self):
self.assertIsNone(settings.NULL_CHOICE_LABEL)
def test_null_choice_value(self):
self.assertEqual(settings.NULL_CHOICE_VALUE, 'null')
class StrictnessTests(TestCase):
class F(FilterSet):
class Meta:
model = User
fields = []
def test_settings_default(self):
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
def test_ignore(self):
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
def test_return_no_results(self):
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RETURN_NO_RESULTS):
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
def test_raise_validation_error(self):
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RAISE_VALIDATION_ERROR):
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
def test_legacy_ignore(self):
with override_settings(FILTERS_STRICTNESS=False):
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
def test_legacy_return_no_results(self):
with override_settings(FILTERS_STRICTNESS=True):
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
def test_legacy_raise_validation_error(self):
with override_settings(FILTERS_STRICTNESS='RAISE'):
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
def test_legacy_differentiation(self):
self.assertNotEqual(STRICTNESS.IGNORE, False)
self.assertNotEqual(STRICTNESS.RETURN_NO_RESULTS, True)
self.assertNotEqual(STRICTNESS.RAISE_VALIDATION_ERROR, 'RAISE')
class OverrideSettingsTests(TestCase):
def test_attribute_override(self):
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
original = settings.VERBOSE_LOOKUPS
with override_settings(FILTERS_VERBOSE_LOOKUPS=None):
self.assertIsNone(settings.VERBOSE_LOOKUPS)
self.assertIs(settings.VERBOSE_LOOKUPS, original)
def test_missing_attribute_override(self):
# ensure that changed setting behaves correctly when
# not originally present in the user's settings.
from django.conf import settings as dj_settings
self.assertFalse(hasattr(dj_settings, 'FILTERS_HELP_TEXT_FILTER'))
# Default value
self.assertTrue(settings.HELP_TEXT_FILTER)
with override_settings(FILTERS_HELP_TEXT_FILTER=None):
self.assertIsNone(settings.HELP_TEXT_FILTER)
# Revert to default
self.assertTrue(settings.HELP_TEXT_FILTER)
def test_non_filters_setting(self):
self.assertFalse(hasattr(settings, 'USE_TZ'))
with override_settings(USE_TZ=False):
self.assertFalse(hasattr(settings, 'USE_TZ'))
self.assertFalse(hasattr(settings, 'USE_TZ'))
def test_non_existent_setting(self):
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
with override_settings(FILTERS_FOOBAR='blah'):
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
class IsCallableTests(TestCase):
def test_behavior(self):
def func():
pass
class Class(object):
def __call__(self):
pass
def method(self):
pass
c = Class()
self.assertTrue(is_callable(func))
self.assertFalse(is_callable(Class))
self.assertTrue(is_callable(c))
self.assertTrue(is_callable(c.method))

View File

@ -0,0 +1,54 @@
import warnings
from django.test import TestCase
from django_filters import FilterSet, filters
class TogetherOptionDeprecationTests(TestCase):
def test_deprecation(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
class Meta:
together = ['a', 'b']
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertIn('The `Meta.together` option has been deprecated', str(w[0].message))
class FilterNameDeprecationTests(TestCase):
def test_declaration(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
class F(FilterSet):
foo = filters.CharFilter(name='foo')
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertIn("`Filter.name` has been renamed to `Filter.field_name`.", str(w[0].message))
def test_name_property(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
filters.CharFilter().name
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertIn("`Filter.name` has been renamed to `Filter.field_name`.", str(w[0].message))
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
filters.CharFilter().name = 'bar'
self.assertEqual(len(w), 1)
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertIn("`Filter.name` has been renamed to `Filter.field_name`.", str(w[0].message))

272
tests/test_fields.py Normal file
View File

@ -0,0 +1,272 @@
from __future__ import absolute_import, unicode_literals
import decimal
from datetime import datetime, time, timedelta, tzinfo
import pytz
from django import forms
from django.test import TestCase, override_settings
from django.utils import timezone
from django_filters.fields import (
BaseCSVField,
BaseRangeField,
DateRangeField,
DateTimeRangeField,
IsoDateTimeField,
Lookup,
LookupTypeField,
RangeField,
TimeRangeField
)
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
def to_d(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):
def test_field(self):
f = RangeField()
self.assertEqual(len(f.fields), 2)
def test_clean(self):
w = RangeWidget()
f = RangeField(widget=w, required=False)
self.assertEqual(
f.clean(['12.34', '55']),
slice(to_d(12.34), to_d(55)))
self.assertIsNone(f.clean([]))
class DateRangeFieldTests(TestCase):
def test_field(self):
f = DateRangeField()
self.assertEqual(len(f.fields), 2)
@override_settings(USE_TZ=False)
def test_clean(self):
w = RangeWidget()
f = DateRangeField(widget=w, required=False)
self.assertEqual(
f.clean(['2015-01-01', '2015-01-10']),
slice(datetime(2015, 1, 1, 0, 0, 0),
datetime(2015, 1, 10, 23, 59, 59, 999999)))
self.assertIsNone(f.clean([]))
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):
def test_field(self):
f = DateRangeField()
self.assertEqual(len(f.fields), 2)
def test_clean(self):
w = RangeWidget()
f = TimeRangeField(widget=w)
self.assertEqual(
f.clean(['10:15', '12:30']),
slice(time(10, 15, 0), time(12, 30, 0)))
class LookupTypeFieldTests(TestCase):
def test_field(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
self.assertEqual(len(f.fields), 2)
def test_clean(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')], required=False)
self.assertEqual(
f.clean(['12.34', 'lt']),
Lookup(to_d(12.34), 'lt'))
self.assertEqual(
f.clean([]),
Lookup(value=None, lookup_type='exact'))
def test_render_used_html5(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
self.assertHTMLEqual(f.widget.render('price', ''), """
<input type="number" step="any" name="price_0" />
<select name="price_1">
<option value="gt">gt</option>
<option value="lt">lt</option>
</select>""")
self.assertHTMLEqual(f.widget.render('price', ['abc', 'lt']), """
<input type="number" step="any" name="price_0" value="abc" />
<select name="price_1">
<option value="gt">gt</option>
<option selected="selected" value="lt">lt</option>
</select>""")
class IsoDateTimeFieldTests(TestCase):
reference_str = "2015-07-19T13:34:51.759"
reference_dt = datetime(2015, 7, 19, 13, 34, 51, 759000)
field = IsoDateTimeField()
def parse_input(self, value):
return self.field.strptime(value, IsoDateTimeField.ISO_8601)
def test_datetime_string_is_parsed(self):
d = self.parse_input(self.reference_str)
self.assertTrue(isinstance(d, datetime))
def test_datetime_string_with_timezone_is_parsed(self):
d = self.parse_input(self.reference_str + "+01:00")
self.assertTrue(isinstance(d, datetime))
def test_datetime_zulu(self):
d = self.parse_input(self.reference_str + "Z")
self.assertTrue(isinstance(d, datetime))
@override_settings(TIME_ZONE='UTC')
def test_datetime_timezone_awareness(self):
utc, tokyo = pytz.timezone('UTC'), pytz.timezone('Asia/Tokyo')
# by default, use the server timezone
reference = utc.localize(self.reference_dt)
parsed = self.parse_input(self.reference_str)
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed, reference)
# if set, use the active timezone
reference = tokyo.localize(self.reference_dt)
with timezone.override(tokyo):
parsed = self.parse_input(self.reference_str)
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed.tzinfo.zone, tokyo.zone)
self.assertEqual(parsed, reference)
# if provided, utc offset should have precedence
reference = utc.localize(self.reference_dt - timedelta(hours=1))
parsed = self.parse_input(self.reference_str + "+01:00")
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed, reference)
@override_settings(USE_TZ=False)
def test_datetime_timezone_naivety(self):
reference = self.reference_dt.replace()
parsed = self.parse_input(self.reference_str + "+01:00")
self.assertIsNone(parsed.tzinfo)
self.assertEqual(parsed, reference - timedelta(hours=1))
parsed = self.parse_input(self.reference_str)
self.assertIsNone(parsed.tzinfo)
self.assertEqual(parsed, reference)
def test_datetime_non_iso_format(self):
f = IsoDateTimeField()
parsed = f.strptime('19-07-2015T51:34:13.759', '%d-%m-%YT%S:%M:%H.%f')
self.assertTrue(isinstance(parsed, datetime))
self.assertEqual(parsed, self.reference_dt)
def test_datetime_wrong_format(self):
with self.assertRaises(ValueError):
self.parse_input('19-07-2015T51:34:13.759')
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'])
def test_derived_widget(self):
with self.assertRaises(AssertionError) as excinfo:
BaseCSVField(widget=RangeWidget())
msg = str(excinfo.exception)
self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
self.assertIn("RangeWidget", msg)
widget = CSVWidget(attrs={'class': 'class'})
field = BaseCSVField(widget=widget)
self.assertIsInstance(field.widget, CSVWidget)
self.assertEqual(field.widget.attrs, {'class': 'class'})
field = BaseCSVField(widget=CSVWidget)
self.assertIsInstance(field.widget, CSVWidget)
field = BaseCSVField(widget=forms.Select)
self.assertIsInstance(field.widget, forms.Select)
self.assertIsInstance(field.widget, BaseCSVWidget)
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'])

1968
tests/test_filtering.py Normal file

File diff suppressed because it is too large Load Diff

1482
tests/test_filters.py Normal file

File diff suppressed because it is too large Load Diff

890
tests/test_filterset.py Normal file
View File

@ -0,0 +1,890 @@
from __future__ import absolute_import, unicode_literals
import mock
import unittest
import django
from django.core.exceptions import ValidationError
from django.db import models
from django.test import TestCase, override_settings
from django_filters.constants import STRICTNESS
from django_filters.filters import (
BaseInFilter,
BaseRangeFilter,
BooleanFilter,
CharFilter,
ChoiceFilter,
DateRangeFilter,
Filter,
FilterMethod,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
NumberFilter,
UUIDFilter
)
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS, FilterSet
from django_filters.widgets import BooleanWidget
from .models import (
Account,
AdminUser,
Article,
BankAccount,
Book,
Business,
Comment,
DirectedNode,
NetworkSetting,
Node,
Profile,
Restaurant,
SubnetMaskField,
User,
UUIDTestModel,
Worker
)
def checkItemsEqual(L1, L2):
"""
TestCase.assertItemsEqual() is not available in Python 2.6.
"""
return len(L1) == len(L2) and sorted(L1) == sorted(L2)
class HelperMethodsTests(TestCase):
@unittest.skip('todo')
def test_get_declared_filters(self):
pass
@unittest.skip('todo')
def test_filters_for_model(self):
pass
@unittest.skip('todo')
def test_filterset_factory(self):
pass
class DbFieldDefaultFiltersTests(TestCase):
def test_expected_db_fields_get_filters(self):
to_check = [
models.BooleanField,
models.CharField,
models.CommaSeparatedIntegerField,
models.DateField,
models.DateTimeField,
models.DecimalField,
models.EmailField,
models.FilePathField,
models.FloatField,
models.IntegerField,
models.GenericIPAddressField,
models.NullBooleanField,
models.PositiveIntegerField,
models.PositiveSmallIntegerField,
models.SlugField,
models.SmallIntegerField,
models.TextField,
models.TimeField,
models.DurationField,
models.URLField,
models.ForeignKey,
models.OneToOneField,
models.ManyToManyField,
models.UUIDField,
]
msg = "%s expected to be found in FILTER_FOR_DBFIELD_DEFAULTS"
for m in to_check:
self.assertIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__)
def test_expected_db_fields_do_not_get_filters(self):
to_check = [
models.Field,
models.BigIntegerField,
models.FileField,
models.ImageField,
]
msg = "%s expected to not be found in FILTER_FOR_DBFIELD_DEFAULTS"
for m in to_check:
self.assertNotIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__)
class FilterSetFilterForFieldTests(TestCase):
def test_filter_found_for_field(self):
f = User._meta.get_field('username')
result = FilterSet.filter_for_field(f, 'username')
self.assertIsInstance(result, CharFilter)
self.assertEqual(result.name, 'username')
def test_filter_found_for_uuidfield(self):
f = UUIDTestModel._meta.get_field('uuid')
result = FilterSet.filter_for_field(f, 'uuid')
self.assertIsInstance(result, UUIDFilter)
self.assertEqual(result.name, 'uuid')
def test_filter_found_for_autofield(self):
f = User._meta.get_field('id')
result = FilterSet.filter_for_field(f, 'id')
self.assertIsInstance(result, NumberFilter)
self.assertEqual(result.name, 'id')
def test_field_with_extras(self):
f = User._meta.get_field('favorite_books')
result = FilterSet.filter_for_field(f, 'favorite_books')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'favorite_books')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, Book)
def test_field_with_choices(self):
f = User._meta.get_field('status')
result = FilterSet.filter_for_field(f, 'status')
self.assertIsInstance(result, ChoiceFilter)
self.assertEqual(result.name, 'status')
self.assertTrue('choices' in result.extra)
self.assertIsNotNone(result.extra['choices'])
def test_field_that_is_subclassed(self):
f = User._meta.get_field('first_name')
result = FilterSet.filter_for_field(f, 'first_name')
self.assertIsInstance(result, CharFilter)
def test_unknown_field_type_error(self):
f = NetworkSetting._meta.get_field('mask')
with self.assertRaises(AssertionError) as excinfo:
FilterSet.filter_for_field(f, 'mask')
self.assertIn(
"FilterSet resolved field 'mask' with 'exact' lookup "
"to an unrecognized field type SubnetMaskField",
excinfo.exception.args[0])
def test_symmetrical_selfref_m2m_field(self):
f = Node._meta.get_field('adjacents')
result = FilterSet.filter_for_field(f, 'adjacents')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'adjacents')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, Node)
def test_non_symmetrical_selfref_m2m_field(self):
f = DirectedNode._meta.get_field('outbound_nodes')
result = FilterSet.filter_for_field(f, 'outbound_nodes')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'outbound_nodes')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, DirectedNode)
def test_m2m_field_with_through_model(self):
f = Business._meta.get_field('employees')
result = FilterSet.filter_for_field(f, 'employees')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'employees')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
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')
def test_filter_overrides(self):
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):
class Meta:
filter_overrides = {
models.BooleanField: {
'filter_class': BooleanFilter,
'extra': lambda f: {
'widget': BooleanWidget,
},
},
}
f = Article._meta.get_field('author')
result, params = OFilterSet.filter_for_lookup(f, 'isnull')
self.assertEqual(result, BooleanFilter)
self.assertEqual(params['widget'], BooleanWidget)
class FilterSetFilterForReverseFieldTests(TestCase):
def test_reverse_o2o_relationship(self):
f = Account._meta.get_field('profile')
result = FilterSet.filter_for_reverse_field(f, 'profile')
self.assertIsInstance(result, ModelChoiceFilter)
self.assertEqual(result.name, 'profile')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, Profile)
def test_reverse_fk_relationship(self):
f = User._meta.get_field('comments')
result = FilterSet.filter_for_reverse_field(f, 'comments')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'comments')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, Comment)
def test_reverse_m2m_relationship(self):
f = Book._meta.get_field('lovers')
result = FilterSet.filter_for_reverse_field(f, 'lovers')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'lovers')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, User)
def test_reverse_non_symmetrical_selfref_m2m_field(self):
f = DirectedNode._meta.get_field('inbound_nodes')
result = FilterSet.filter_for_reverse_field(f, 'inbound_nodes')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'inbound_nodes')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, DirectedNode)
def test_reverse_m2m_field_with_through_model(self):
f = Worker._meta.get_field('employers')
result = FilterSet.filter_for_reverse_field(f, 'employers')
self.assertIsInstance(result, ModelMultipleChoiceFilter)
self.assertEqual(result.name, 'employers')
self.assertTrue('queryset' in result.extra)
self.assertIsNotNone(result.extra['queryset'])
self.assertEqual(result.extra['queryset'].model, Business)
class FilterSetClassCreationTests(TestCase):
def test_no_filters(self):
class F(FilterSet):
pass
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 0)
def test_declaring_filter(self):
class F(FilterSet):
username = CharFilter()
self.assertEqual(len(F.declared_filters), 1)
self.assertListEqual(list(F.declared_filters), ['username'])
self.assertEqual(len(F.base_filters), 1)
self.assertListEqual(list(F.base_filters), ['username'])
def test_model_derived(self):
class F(FilterSet):
class Meta:
model = Book
fields = '__all__'
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 3)
self.assertListEqual(list(F.base_filters),
['title', 'price', 'average_rating'])
def test_model_no_fields_or_exclude(self):
with self.assertRaises(AssertionError) as excinfo:
class F(FilterSet):
class Meta:
model = Book
self.assertIn(
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'",
str(excinfo.exception)
)
def test_model_fields_empty(self):
class F(FilterSet):
class Meta:
model = Book
fields = []
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 0)
self.assertListEqual(list(F.base_filters), [])
def test_model_exclude_empty(self):
# equivalent to fields = '__all__'
class F(FilterSet):
class Meta:
model = Book
exclude = []
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 3)
self.assertListEqual(list(F.base_filters),
['title', 'price', 'average_rating'])
def test_declared_and_model_derived(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = '__all__'
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 4)
self.assertListEqual(list(F.base_filters),
['title', 'price', 'average_rating', 'username'])
def test_meta_fields_with_declared_and_model_derived(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = ('username', 'price')
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 2)
self.assertListEqual(list(F.base_filters), ['username', 'price'])
def test_meta_fields_dictionary_derived(self):
class F(FilterSet):
class Meta:
model = Book
fields = {'price': ['exact', 'gte', 'lte'], }
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 3)
expected_list = ['price', 'price__gte', 'price__lte', ]
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
def test_meta_fields_containing_autofield(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = ('id', 'username', 'price')
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 3)
self.assertListEqual(list(F.base_filters), ['id', 'username', 'price'])
def test_meta_fields_dictionary_autofield(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = {'id': ['exact'],
'username': ['exact'],
}
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 2)
expected_list = ['id', 'username']
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
def test_meta_fields_containing_unknown(self):
with self.assertRaises(TypeError) as excinfo:
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = ('username', 'price', 'other', 'another')
self.assertEqual(
str(excinfo.exception),
"'Meta.fields' contains fields that are not defined on this FilterSet: "
"other, another"
)
def test_meta_fields_dictionary_containing_unknown(self):
with self.assertRaises(TypeError):
class F(FilterSet):
class Meta:
model = Book
fields = {'id': ['exact'],
'title': ['exact'],
'other': ['exact'],
}
def test_meta_exlude_with_declared_and_declared_wins(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
exclude = ('username', 'price')
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 3)
self.assertListEqual(list(F.base_filters),
['title', 'average_rating', 'username'])
def test_meta_fields_and_exlude_and_exclude_wins(self):
class F(FilterSet):
username = CharFilter()
class Meta:
model = Book
fields = ('username', 'title', 'price')
exclude = ('title',)
self.assertEqual(len(F.declared_filters), 1)
self.assertEqual(len(F.base_filters), 2)
self.assertListEqual(list(F.base_filters),
['username', 'price'])
def test_meta_exlude_with_no_fields(self):
class F(FilterSet):
class Meta:
model = Book
exclude = ('price', )
self.assertEqual(len(F.declared_filters), 0)
self.assertEqual(len(F.base_filters), 2)
self.assertListEqual(list(F.base_filters),
['title', 'average_rating'])
def test_filterset_class_inheritance(self):
class F(FilterSet):
class Meta:
model = Book
fields = '__all__'
class G(F):
pass
self.assertEqual(set(F.base_filters), set(G.base_filters))
class F(FilterSet):
other = CharFilter
class Meta:
model = Book
fields = '__all__'
class G(F):
pass
self.assertEqual(set(F.base_filters), set(G.base_filters))
def test_abstract_model_inheritance(self):
class F(FilterSet):
class Meta:
model = Restaurant
fields = '__all__'
self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza']))
class F(FilterSet):
class Meta:
model = Restaurant
fields = ['name', 'serves_pizza']
self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza']))
def test_custom_field_gets_filter_from_override(self):
class F(FilterSet):
class Meta:
model = NetworkSetting
fields = '__all__'
filter_overrides = {
SubnetMaskField: {'filter_class': CharFilter}
}
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr'])
def test_custom_declared_field_no_warning(self):
class F(FilterSet):
mask = CharFilter()
class Meta:
model = NetworkSetting
fields = ['mask']
self.assertEqual(list(F.base_filters.keys()), ['mask'])
def test_filterset_for_proxy_model(self):
class F(FilterSet):
class Meta:
model = User
fields = '__all__'
class ProxyF(FilterSet):
class Meta:
model = AdminUser
fields = '__all__'
self.assertEqual(list(F.base_filters), list(ProxyF.base_filters))
def test_filterset_for_mti_model(self):
class F(FilterSet):
class Meta:
model = Account
fields = '__all__'
class FtiF(FilterSet):
class Meta:
model = BankAccount
fields = '__all__'
# fails due to 'account_ptr' getting picked up
self.assertEqual(
list(F.base_filters) + ['amount_saved'],
list(FtiF.base_filters))
def test_declared_filter_disabling(self):
class Parent(FilterSet):
f1 = CharFilter()
f2 = CharFilter()
class Child(Parent):
f1 = None
class Grandchild(Child):
pass
self.assertEqual(len(Parent.base_filters), 2)
self.assertEqual(len(Child.base_filters), 1)
self.assertEqual(len(Grandchild.base_filters), 1)
class FilterSetInstantiationTests(TestCase):
def test_creating_instance(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username']
f = F()
self.assertFalse(f.is_bound)
self.assertIsNotNone(f.queryset)
self.assertEqual(len(f.filters), len(F.base_filters))
for name, filter_ in f.filters.items():
self.assertEqual(
filter_.model,
User,
"%s does not have model set correctly" % name)
def test_creating_bound_instance(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username']
f = F({'username': 'username'})
self.assertTrue(f.is_bound)
def test_creating_with_queryset(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username']
m = mock.Mock()
f = F(queryset=m)
self.assertEqual(f.queryset, m)
def test_creating_with_request(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username']
m = mock.Mock()
f = F(request=m)
self.assertEqual(f.request, m)
class FilterSetStrictnessTests(TestCase):
def test_settings_default(self):
class F(FilterSet):
class Meta:
model = User
fields = []
# Ensure default is not IGNORE
self.assertEqual(F().strict, STRICTNESS.RETURN_NO_RESULTS)
# override and test
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
self.assertEqual(F().strict, STRICTNESS.IGNORE)
def test_meta_value(self):
class F(FilterSet):
class Meta:
model = User
fields = []
strict = STRICTNESS.IGNORE
self.assertEqual(F().strict, STRICTNESS.IGNORE)
def test_init_default(self):
class F(FilterSet):
class Meta:
model = User
fields = []
strict = STRICTNESS.IGNORE
strict = STRICTNESS.RAISE_VALIDATION_ERROR
self.assertEqual(F(strict=strict).strict, strict)
def test_legacy_value(self):
class F(FilterSet):
class Meta:
model = User
fields = []
self.assertEqual(F(strict=False).strict, STRICTNESS.IGNORE)
class FilterSetTogetherTests(TestCase):
def setUp(self):
self.alex = User.objects.create(username='alex', status=1)
self.jacob = User.objects.create(username='jacob', status=2)
self.qs = User.objects.all().order_by('id')
def test_fields_set(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status', 'is_active', 'first_name']
together = [
('username', 'status'),
('first_name', 'is_active'),
]
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F({}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex'}, queryset=self.qs)
with self.assertRaises(ValidationError):
f.qs.count()
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
self.assertEqual(f.qs.count(), 1)
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
def test_single_fields_set(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
together = ['username', 'status']
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F({}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex'}, queryset=self.qs)
with self.assertRaises(ValidationError):
f.qs.count()
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
self.assertEqual(f.qs.count(), 1)
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
def test_empty_values(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
together = ['username', 'status']
f = F({'username': '', 'status': ''}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex', 'status': ''}, queryset=self.qs)
self.assertEqual(f.qs.count(), 0)
# test filter.method here, as it depends on its parent FilterSet
class FilterMethodTests(TestCase):
def test_none(self):
# use a mock to bypass bound/unbound method equality
class TestFilter(Filter):
filter = mock.Mock()
f = TestFilter(method=None)
self.assertIsNone(f.method)
# passing method=None should not modify filter function
self.assertIs(f.filter, TestFilter.filter)
def test_method_name(self):
class F(FilterSet):
f = Filter(method='filter_f')
def filter_f(self, qs, name, value):
pass
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, 'filter_f')
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
def test_method_callable(self):
def filter_f(qs, name, value):
pass
class F(FilterSet):
f = Filter(method=filter_f)
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, filter_f)
self.assertEqual(f.filters['f'].filter.method, filter_f)
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
def test_request_available_during_method_called(self):
class F(FilterSet):
f = Filter(method='filter_f')
def filter_f(self, qs, name, value):
# call mock request object to prove self.request can be accessed
self.request()
m = mock.Mock()
f = F({}, queryset=User.objects.all(), request=m)
# call the filter
f.filters['f'].filter.method(User.objects.all(), 'f', '')
m.assert_called_once_with()
def test_method_with_overridden_filter(self):
# Some filter classes override the base filter() method. We need
# to ensure that passing a method argument still works correctly
class F(FilterSet):
f = DateRangeFilter(method='filter_f')
def filter_f(self, qs, name, value):
pass
f = F({}, queryset=User.objects.all())
self.assertEqual(f.filters['f'].method, 'filter_f')
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
def test_parent_unresolvable(self):
f = Filter(method='filter_f')
with self.assertRaises(AssertionError) as w:
f.filter(User.objects.all(), 0)
self.assertIn("'None'", str(w.exception))
self.assertIn('parent', str(w.exception))
self.assertIn('filter_f', str(w.exception))
def test_method_self_is_parent(self):
# Ensure the method isn't 're-parented' on the `FilterMethod` helper class.
# Filter methods should have access to the filterset's properties.
request = mock.Mock()
class F(FilterSet):
f = CharFilter(method='filter_f')
class Meta:
model = User
fields = []
def filter_f(inner_self, qs, name, value):
self.assertIsInstance(inner_self, F)
self.assertIs(inner_self.request, request)
F({'f': 'foo'}, request=request, queryset=User.objects.all()).qs
def test_method_unresolvable(self):
class F(FilterSet):
f = Filter(method='filter_f')
f = F({}, queryset=User.objects.all())
with self.assertRaises(AssertionError) as w:
f.filters['f'].filter(User.objects.all(), 0)
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
self.assertIn('.filter_f()', str(w.exception))
def test_method_uncallable(self):
class F(FilterSet):
f = Filter(method='filter_f')
filter_f = 4
f = F({}, queryset=User.objects.all())
with self.assertRaises(AssertionError) as w:
f.filters['f'].filter(User.objects.all(), 0)
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
self.assertIn('.filter_f()', str(w.exception))
def test_method_set_unset(self):
# use a mock to bypass bound/unbound method equality
class TestFilter(Filter):
filter = mock.Mock()
f = TestFilter(method='filter_f')
self.assertEqual(f.method, 'filter_f')
self.assertIsInstance(f.filter, FilterMethod)
# setting None should revert to Filter.filter
f.method = None
self.assertIsNone(f.method)
self.assertIs(f.filter, TestFilter.filter)
class MiscFilterSetTests(TestCase):
def test_no__getitem__(self):
# The DTL processes variable lookups by the following rules:
# https://docs.djangoproject.com/en/1.9/ref/templates/language/#variables
# A __getitem__ implementation precedes normal attribute access, and in
# the case of #58, will force the queryset to evaluate when it should
# not (eg, when rendering a blank form).
self.assertFalse(hasattr(FilterSet, '__getitem__'))
def test_no_qs_proxying(self):
# The FilterSet should not proxy .qs methods - just access .qs directly
self.assertFalse(hasattr(FilterSet, '__len__'))
self.assertFalse(hasattr(FilterSet, '__iter__'))

227
tests/test_forms.py Normal file
View File

@ -0,0 +1,227 @@
from __future__ import absolute_import, unicode_literals
from django import forms
from django.test import TestCase, override_settings
from django_filters.filters import CharFilter, ChoiceFilter
from django_filters.filterset import FilterSet
from .models import MANAGER, REGULAR, STATUS_CHOICES, Book, ManagerGroup, User
class FilterSetFormTests(TestCase):
def test_form_from_empty_filterset(self):
class F(FilterSet):
pass
f = F(queryset=Book.objects.all()).form
self.assertIsInstance(f, forms.Form)
def test_form(self):
class F(FilterSet):
class Meta:
model = Book
fields = ('title',)
f = F().form
self.assertIsInstance(f, forms.Form)
self.assertEqual(list(f.fields), ['title'])
def test_custom_form(self):
class MyForm(forms.Form):
pass
class F(FilterSet):
class Meta:
model = Book
fields = '__all__'
form = MyForm
f = F().form
self.assertIsInstance(f, MyForm)
def test_form_prefix(self):
class F(FilterSet):
class Meta:
model = Book
fields = ('title',)
f = F().form
self.assertIsNone(f.prefix)
f = F(prefix='prefix').form
self.assertEqual(f.prefix, 'prefix')
def test_form_fields(self):
class F(FilterSet):
class Meta:
model = User
fields = ['status']
f = F().form
self.assertEqual(len(f.fields), 1)
self.assertIn('status', f.fields)
self.assertSequenceEqual(
list(f.fields['status'].choices),
(('', '---------'), ) + STATUS_CHOICES
)
def test_form_fields_exclusion(self):
class F(FilterSet):
title = CharFilter(exclude=True)
class Meta:
model = Book
fields = ('title',)
f = F().form
self.assertEqual(f.fields['title'].label, "Exclude title")
def test_complex_form_fields(self):
class F(FilterSet):
username = CharFilter(label='Filter for users with username')
exclude_username = CharFilter(name='username', lookup_expr='iexact', exclude=True)
class Meta:
model = User
fields = {
'status': ['exact', 'lt', 'gt'],
'favorite_books__title': ['iexact', 'in'],
'manager_of__users__username': ['exact'],
}
fields = F().form.fields
self.assertEqual(fields['username'].label, 'Filter for users with username')
self.assertEqual(fields['exclude_username'].label, 'Exclude username')
self.assertEqual(fields['status'].label, 'Status')
self.assertEqual(fields['status__lt'].label, 'Status is less than')
self.assertEqual(fields['status__gt'].label, 'Status is greater than')
self.assertEqual(fields['favorite_books__title__iexact'].label, 'Favorite books title')
self.assertEqual(fields['favorite_books__title__in'].label, 'Favorite books title is in')
self.assertEqual(fields['manager_of__users__username'].label, 'Manager of users username')
def test_form_fields_using_widget(self):
class F(FilterSet):
status = ChoiceFilter(widget=forms.RadioSelect,
choices=STATUS_CHOICES,
empty_label=None)
class Meta:
model = User
fields = ['status', 'username']
f = F().form
self.assertEqual(len(f.fields), 2)
self.assertIn('status', f.fields)
self.assertIn('username', f.fields)
self.assertSequenceEqual(
list(f.fields['status'].choices),
STATUS_CHOICES
)
self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect)
def test_form_field_with_custom_label(self):
class F(FilterSet):
title = CharFilter(label="Book title")
class Meta:
model = Book
fields = ('title',)
f = F().form
self.assertEqual(f.fields['title'].label, "Book title")
self.assertEqual(f['title'].label, 'Book title')
def test_form_field_with_manual_name(self):
class F(FilterSet):
book_title = CharFilter(name='title')
class Meta:
model = Book
fields = ('book_title',)
f = F().form
self.assertEqual(f.fields['book_title'].label, "Title")
self.assertEqual(f['book_title'].label, "Title")
def test_form_field_with_manual_name_and_label(self):
class F(FilterSet):
f1 = CharFilter(name='title', label="Book title")
class Meta:
model = Book
fields = ('f1',)
f = F().form
self.assertEqual(f.fields['f1'].label, "Book title")
self.assertEqual(f['f1'].label, 'Book title')
def test_filter_with_initial(self):
class F(FilterSet):
status = ChoiceFilter(choices=STATUS_CHOICES, initial=1)
class Meta:
model = User
fields = ['status']
f = F().form
self.assertEqual(f.fields['status'].initial, 1)
def test_form_is_not_bound(self):
class F(FilterSet):
class Meta:
model = Book
fields = ('title',)
f = F().form
self.assertFalse(f.is_bound)
self.assertEqual(f.data, {})
def test_form_is_bound(self):
class F(FilterSet):
class Meta:
model = Book
fields = ('title',)
f = F({'title': 'Some book'}).form
self.assertTrue(f.is_bound)
self.assertEqual(f.data, {'title': 'Some book'})
def test_limit_choices_to(self):
User.objects.create(username='inactive', is_active=False, status=REGULAR)
User.objects.create(username='active', is_active=True, status=REGULAR)
User.objects.create(username='manager', is_active=False, status=MANAGER)
class F(FilterSet):
class Meta:
model = ManagerGroup
fields = ['users', 'manager']
f = F().form
self.assertEqual(
list(f.fields['users'].choices), [(2, 'active')]
)
self.assertEqual(
list(f.fields['manager'].choices), [('', '---------'), (3, 'manager')]
)
def test_disabled_help_text(self):
class F(FilterSet):
class Meta:
model = Book
fields = {
# 'in' lookups are CSV-based, which have a `help_text`.
'title': ['in']
}
self.assertEqual(
F().form.fields['title__in'].help_text,
'Multiple values may be separated by commas.'
)
with override_settings(FILTERS_DISABLE_HELP_TEXT=True):
self.assertEqual(
F().form.fields['title__in'].help_text,
''
)

368
tests/test_utils.py Normal file
View File

@ -0,0 +1,368 @@
import datetime
import unittest
import django
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.related import ForeignObjectRel
from django.forms import ValidationError
from django.test import TestCase, override_settings
from django.utils.functional import Promise
from django.utils.timezone import get_default_timezone
from django_filters import STRICTNESS, FilterSet
from django_filters.exceptions import FieldLookupError
from django_filters.utils import (
get_field_parts,
get_model_field,
handle_timezone,
label_for_filter,
raw_validation,
resolve_field,
verbose_field_name,
verbose_lookup_expr
)
from .models import (
Account,
Article,
Book,
Business,
Company,
HiredWorker,
NetworkSetting,
User
)
class GetFieldPartsTests(TestCase):
def test_field(self):
parts = get_field_parts(User, 'username')
self.assertEqual(len(parts), 1)
self.assertIsInstance(parts[0], models.CharField)
def test_non_existent_field(self):
result = get_model_field(User, 'unknown__name')
self.assertIsNone(result)
def test_forwards_related_field(self):
parts = get_field_parts(User, 'favorite_books__title')
self.assertEqual(len(parts), 2)
self.assertIsInstance(parts[0], models.ManyToManyField)
self.assertIsInstance(parts[1], models.CharField)
def test_reverse_related_field(self):
parts = get_field_parts(User, 'manager_of__users__username')
self.assertEqual(len(parts), 3)
self.assertIsInstance(parts[0], ForeignObjectRel)
self.assertIsInstance(parts[1], models.ManyToManyField)
self.assertIsInstance(parts[2], models.CharField)
class GetModelFieldTests(TestCase):
def test_non_existent_field(self):
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(FieldLookupError) as context:
resolve_field(model_field, 'invalid_lookup')
exc = str(context.exception)
self.assertIn(str(model_field), exc)
self.assertIn('invalid_lookup', exc)
def test_invalid_transformed_lookup_expression(self):
model_field = Article._meta.get_field('published')
with self.assertRaises(FieldLookupError) as context:
resolve_field(model_field, 'date__invalid_lookup')
exc = str(context.exception)
self.assertIn(str(model_field), exc)
self.assertIn('date__invalid_lookup', exc)
class VerboseFieldNameTests(TestCase):
def test_none(self):
verbose_name = verbose_field_name(Article, None)
self.assertEqual(verbose_name, '[invalid name]')
def test_invalid_name(self):
verbose_name = verbose_field_name(Article, 'foobar')
self.assertEqual(verbose_name, '[invalid name]')
def test_field(self):
verbose_name = verbose_field_name(Article, 'author')
self.assertEqual(verbose_name, 'author')
def test_field_with_verbose_name(self):
verbose_name = verbose_field_name(Article, 'name')
self.assertEqual(verbose_name, 'title')
def test_field_all_caps(self):
verbose_name = verbose_field_name(NetworkSetting, 'cidr')
self.assertEqual(verbose_name, 'CIDR')
def test_forwards_related_field(self):
verbose_name = verbose_field_name(Article, 'author__username')
self.assertEqual(verbose_name, 'author username')
def test_backwards_related_field(self):
verbose_name = verbose_field_name(Book, 'lovers__first_name')
self.assertEqual(verbose_name, 'lovers first name')
def test_backwards_related_field_multi_word(self):
verbose_name = verbose_field_name(User, 'manager_of')
self.assertEqual(verbose_name, 'manager of')
def test_lazy_text(self):
# sanity check
field = User._meta.get_field('username')
self.assertIsInstance(field.verbose_name, Promise)
verbose_name = verbose_field_name(User, 'username')
self.assertEqual(verbose_name, 'username')
def test_forwards_fk(self):
verbose_name = verbose_field_name(Article, 'author')
self.assertEqual(verbose_name, 'author')
def test_backwards_fk(self):
# https://github.com/carltongibson/django-filter/issues/716
# related_name is set
verbose_name = verbose_field_name(Company, 'locations')
self.assertEqual(verbose_name, 'locations')
# related_name not set. Auto-generated relation is `article_set`
# _meta.get_field raises FieldDoesNotExist
verbose_name = verbose_field_name(User, 'article_set')
self.assertEqual(verbose_name, '[invalid name]')
# WRONG NAME! Returns ManyToOneRel with related_name == None.
verbose_name = verbose_field_name(User, 'article')
self.assertEqual(verbose_name, '[invalid name]')
class VerboseLookupExprTests(TestCase):
def test_exact(self):
# Exact should default to empty. A verbose expression is unnecessary,
# and this behavior works well with list syntax for `Meta.fields`.
verbose_lookup = verbose_lookup_expr('exact')
self.assertEqual(verbose_lookup, '')
def test_verbose_expression(self):
verbose_lookup = verbose_lookup_expr('date__lt')
self.assertEqual(verbose_lookup, 'date is less than')
def test_missing_keys(self):
verbose_lookup = verbose_lookup_expr('foo__bar__lt')
self.assertEqual(verbose_lookup, 'foo bar is less than')
@override_settings(FILTERS_VERBOSE_LOOKUPS={'exact': 'is equal to'})
def test_overridden_settings(self):
verbose_lookup = verbose_lookup_expr('exact')
self.assertEqual(verbose_lookup, 'is equal to')
class LabelForFilterTests(TestCase):
def test_standard_label(self):
label = label_for_filter(Article, 'name', 'in')
self.assertEqual(label, 'Title is in')
def test_related_model(self):
label = label_for_filter(Article, 'author__first_name', 'in')
self.assertEqual(label, 'Author first name is in')
def test_exclusion_label(self):
label = label_for_filter(Article, 'name', 'in', exclude=True)
self.assertEqual(label, 'Exclude title is in')
def test_related_model_exclusion(self):
label = label_for_filter(Article, 'author__first_name', 'in', exclude=True)
self.assertEqual(label, 'Exclude author first name is in')
def test_exact_lookup(self):
label = label_for_filter(Article, 'name', 'exact')
self.assertEqual(label, 'Title')
def test_field_all_caps(self):
label = label_for_filter(NetworkSetting, 'cidr', 'contains', exclude=True)
self.assertEqual(label, 'Exclude CIDR contains')
class HandleTimezone(TestCase):
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_handle_dst_ending(self):
dst_ending_date = datetime.datetime(2017, 2, 18, 23, 59, 59, 999999)
handled = handle_timezone(dst_ending_date, False)
self.assertEqual(handled, get_default_timezone().localize(dst_ending_date, False))
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_handle_dst_starting(self):
dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0)
handled = handle_timezone(dst_starting_date, True)
self.assertEqual(handled, get_default_timezone().localize(dst_starting_date, True))
class RawValidationDataTests(TestCase):
def test_simple(self):
class F(FilterSet):
class Meta:
model = Article
fields = ['id', 'author', 'name']
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'})
with self.assertRaises(ValidationError) as exc:
f.qs
self.assertDictEqual(raw_validation(exc.exception), {
'id': ['Enter a number.'],
'author': ['Select a valid choice. That choice is not one of the available choices.'],
})

109
tests/test_views.py Normal file
View File

@ -0,0 +1,109 @@
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from django.test.client import RequestFactory
from django_filters.filterset import FilterSet, filterset_factory
from django_filters.views import FilterView
from .models import Book
@override_settings(ROOT_URLCONF='tests.urls')
class GenericViewTestCase(TestCase):
def setUp(self):
Book.objects.create(
title="Ender's Game", price='1.00', average_rating=3.0)
Book.objects.create(
title="Rainbow Six", price='1.00', average_rating=3.0)
Book.objects.create(
title="Snowcrash", price='1.00', average_rating=3.0)
class GenericClassBasedViewTests(GenericViewTestCase):
base_url = '/books/'
def test_view(self):
response = self.client.get(self.base_url)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_filtering_on_title(self):
response = self.client.get(self.base_url + '?title=Snowcrash')
for b in ['Ender&#39;s Game', 'Rainbow Six']:
self.assertNotContains(response, b)
self.assertContains(response, 'Snowcrash')
def test_view_with_filterset_not_model(self):
factory = RequestFactory()
request = factory.get(self.base_url)
filterset = filterset_factory(Book)
view = FilterView.as_view(filterset_class=filterset)
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_with_model_no_filterset(self):
factory = RequestFactory()
request = factory.get(self.base_url)
view = FilterView.as_view(model=Book)
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_with_model_and_fields_no_filterset(self):
factory = RequestFactory()
request = factory.get(self.base_url + '?price=1.0')
view = FilterView.as_view(model=Book, filter_fields=['price'])
# filtering only by price
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
# not filtering by title
request = factory.get(self.base_url + '?title=Snowcrash')
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_without_filterset_or_model(self):
factory = RequestFactory()
request = factory.get(self.base_url)
view = FilterView.as_view()
with self.assertRaises(ImproperlyConfigured):
view(request)
def test_view_with_bad_filterset(self):
class MyFilterSet(FilterSet):
pass
factory = RequestFactory()
request = factory.get(self.base_url)
view = FilterView.as_view(filterset_class=MyFilterSet)
with self.assertRaises(ImproperlyConfigured):
view(request)
class GenericFunctionalViewTests(GenericViewTestCase):
base_url = '/books-legacy/'
def test_view(self):
response = self.client.get(self.base_url)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
# extra context
self.assertEqual(response.context_data['foo'], 'bar')
self.assertEqual(response.context_data['bar'], 'foo')
def test_view_filtering_on_price(self):
response = self.client.get(self.base_url + '?title=Snowcrash')
for b in ['Ender&#39;s Game', 'Rainbow Six']:
self.assertNotContains(response, b)
self.assertContains(response, 'Snowcrash')

422
tests/test_widgets.py Normal file
View File

@ -0,0 +1,422 @@
from __future__ import absolute_import, unicode_literals
from django.forms import Select, TextInput
from django.test import TestCase
from django_filters.widgets import (
BaseCSVWidget,
BooleanWidget,
CSVWidget,
LinkWidget,
LookupTypeWidget,
QueryArrayWidget,
RangeWidget,
SuffixedMultiWidget
)
class LookupTypeWidgetTests(TestCase):
def test_widget_requires_field(self):
with self.assertRaises(TypeError):
LookupTypeWidget()
def test_widget_render(self):
widgets = [TextInput(), Select(choices=(('a', 'a'), ('b', 'b')))]
w = LookupTypeWidget(widgets)
self.assertHTMLEqual(w.render('price', ''), """
<input name="price_0" type="text" />
<select name="price_1">
<option value="a">a</option>
<option value="b">b</option>
</select>""")
self.assertHTMLEqual(w.render('price', None), """
<input name="price_0" type="text" />
<select name="price_1">
<option value="a">a</option>
<option value="b">b</option>
</select>""")
self.assertHTMLEqual(w.render('price', ['2', 'a']), """
<input name="price_0" type="text" value="2" />
<select name="price_1">
<option selected="selected" value="a">a</option>
<option value="b">b</option>
</select>""")
class LinkWidgetTests(TestCase):
def test_widget_without_choices(self):
w = LinkWidget()
self.assertEqual(len(w.choices), 0)
self.assertHTMLEqual(w.render('price', ''), """<ul />""")
def test_widget(self):
choices = (
('test-val1', 'test-label1'),
('test-val2', 'test-label2'),
)
w = LinkWidget(choices=choices)
self.assertEqual(len(w.choices), 2)
self.assertHTMLEqual(w.render('price', ''), """
<ul>
<li><a href="?price=test-val1">test-label1</a></li>
<li><a href="?price=test-val2">test-label2</a></li>
</ul>""")
self.assertHTMLEqual(w.render('price', None), """
<ul>
<li><a href="?price=test-val1">test-label1</a></li>
<li><a href="?price=test-val2">test-label2</a></li>
</ul>""")
self.assertHTMLEqual(w.render('price', 'test-val1'), """
<ul>
<li><a class="selected"
href="?price=test-val1">test-label1</a></li>
<li><a href="?price=test-val2">test-label2</a></li>
</ul>""")
def test_widget_with_option_groups(self):
choices = (
('Audio', (
('vinyl', 'Vinyl'),
('cd', 'CD'),
)),
('Video', (
('vhs', 'VHS Tape'),
('dvd', 'DVD'),
)),
('unknown', 'Unknown'),
)
w = LinkWidget(choices=choices)
self.assertHTMLEqual(w.render('media', ''), """
<ul>
<li><a href="?media=vinyl">Vinyl</a></li>
<li><a href="?media=cd">CD</a></li>
<li><a href="?media=vhs">VHS Tape</a></li>
<li><a href="?media=dvd">DVD</a></li>
<li><a href="?media=unknown">Unknown</a></li>
</ul>""")
def test_widget_with_blank_choice(self):
choices = (
('', '---------'),
('test-val1', 'test-label1'),
('test-val2', 'test-label2'),
)
w = LinkWidget(choices=choices)
self.assertHTMLEqual(w.render('price', ''), """
<ul>
<li><a class="selected" href="?price=">All</a></li>
<li><a href="?price=test-val1">test-label1</a></li>
<li><a href="?price=test-val2">test-label2</a></li>
</ul>""")
def test_widget_value_from_datadict(self):
w = LinkWidget()
data = {'price': 'test-val1'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, 'test-val1')
class SuffixedMultiWidgetTests(TestCase):
def test_assertions(self):
# number of widgets must match suffixes
with self.assertRaises(AssertionError):
SuffixedMultiWidget(widgets=[BooleanWidget])
# suffixes must be unique
class W(SuffixedMultiWidget):
suffixes = ['a', 'a']
with self.assertRaises(AssertionError):
W(widgets=[BooleanWidget, BooleanWidget])
# should succeed
class W(SuffixedMultiWidget):
suffixes = ['a', 'b']
W(widgets=[BooleanWidget, BooleanWidget])
def test_render(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']
w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price_min" type="text" />
<input name="price_max" type="text" />
""")
# blank suffix
class W(SuffixedMultiWidget):
suffixes = [None, 'lookup']
w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price" type="text" />
<input name="price_lookup" type="text" />
""")
def test_value_from_datadict(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']
w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price_min': '1',
'price_max': '2',
}, {}, 'price')
self.assertEqual(result, ['1', '2'])
result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, [None, None])
# blank suffix
class W(SuffixedMultiWidget):
suffixes = ['', 'lookup']
w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price': '1',
'price_lookup': 'lt',
}, {}, 'price')
self.assertEqual(result, ['1', 'lt'])
class RangeWidgetTests(TestCase):
def test_widget(self):
w = RangeWidget()
self.assertEqual(len(w.widgets), 2)
self.assertHTMLEqual(w.render('price', ''), """
<input type="text" name="price_0" />
-
<input type="text" name="price_1" />""")
self.assertHTMLEqual(w.render('price', slice(5.99, 9.99)), """
<input type="text" name="price_0" value="5.99" />
-
<input type="text" name="price_1" value="9.99" />""")
def test_widget_attributes(self):
w = RangeWidget(attrs={'type': 'date'})
self.assertEqual(len(w.widgets), 2)
self.assertHTMLEqual(w.render('date', ''), """
<input type="date" name="date_0" />
-
<input type="date" name="date_1" />""")
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):
"""
"""
w = BooleanWidget()
trueActive = {'active': 'true'}
result = w.value_from_datadict(trueActive, {}, 'active')
self.assertEqual(result, True)
falseActive = {'active': 'false'}
result = w.value_from_datadict(falseActive, {}, 'active')
self.assertEqual(result, False)
result = w.value_from_datadict({}, {}, 'active')
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', []), """
<input type="text" name="price" />""")
self.assertHTMLEqual(w.render('price', '1'), """
<input type="text" name="price" value="1" />""")
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': '1,'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['1', ''])
data = {'price': ','}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['', ''])
data = {'price': ''}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, None)
class CSVSelectTests(TestCase):
class CSVSelect(BaseCSVWidget, Select):
pass
def test_widget(self):
w = self.CSVSelect(choices=((1, 'a'), (2, 'b')))
self.assertHTMLEqual(
w.render('price', None),
"""
<select name="price">
<option value="1">a</option>
<option value="2">b</option>
</select>
"""
)
self.assertHTMLEqual(
w.render('price', ''),
"""
<select name="price">
<option value="1">a</option>
<option value="2">b</option>
</select>
""")
self.assertHTMLEqual(
w.render('price', '1'),
"""
<select name="price">
<option selected="selected" value="1">a</option>
<option value="2">b</option>
</select>
""")
self.assertHTMLEqual(
w.render('price', '1,2'),
"""
<select name="price">
<option value="1">a</option>
<option value="2">b</option>
</select>
"""
)
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" />""")
class QueryArrayWidgetTests(TestCase):
def test_widget_value_from_datadict(self):
w = QueryArrayWidget()
# Values can be provided as csv string: ?foo=bar,baz
data = {'price': None}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
data = {'price': '1'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['1'])
data = {'price': '1,2'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(sorted(result), ['1', '2'])
data = {'price': '1,,2'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(sorted(result), ['1', '2'])
data = {'price': '1,'}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['1'])
data = {'price': ','}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
data = {'price': ''}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, [])
# Values can be provided as query array: ?foo[]=bar&foo[]=baz
data = {'price[]': None}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
data = {'price[]': ['1']}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['1'])
data = {'price[]': ['1', '2']}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(sorted(result), ['1', '2'])
data = {'price[]': ['1', '', '2']}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(sorted(result), ['1', '2'])
data = {'price[]': ['1', '']}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, ['1'])
data = {'price[]': ['', '']}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
data = {'price[]': []}
result = w.value_from_datadict(data, {}, 'price')
self.assertEqual(result, [])
result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, [])

16
tests/urls.py Normal file
View File

@ -0,0 +1,16 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from django_filters.views import FilterView, object_filter
from .models import Book
def _foo():
return 'bar'
urlpatterns = [
url(r'^books-legacy/$', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}),
url(r'^books/$', FilterView.as_view(model=Book)),
]