Compare commits
No commits in common. "master" and "sid" have entirely different histories.
|
@ -1,21 +0,0 @@
|
|||
[bumpversion]
|
||||
current_version = 1.0.1
|
||||
commit = False
|
||||
tag = False
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
|
||||
serialize =
|
||||
{major}.{minor}.{patch}-{release}
|
||||
{major}.{minor}.{patch}
|
||||
|
||||
[bumpversion:file:django_filters/__init__.py]
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
[bumpversion:part:release]
|
||||
optional_value = gamma
|
||||
values =
|
||||
dev
|
||||
gamma
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
*.pyc
|
||||
*.egg-info
|
||||
build/
|
||||
dist/
|
||||
docs/_build
|
||||
.python-version
|
||||
.tox
|
16
.travis.yml
16
.travis.yml
|
@ -1,16 +0,0 @@
|
|||
sudo: false
|
||||
|
||||
language: python
|
||||
python: '3.5'
|
||||
cache: pip
|
||||
|
||||
install:
|
||||
- pip install coverage tox
|
||||
|
||||
script:
|
||||
- coverage erase
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- coverage combine
|
||||
- coverage report
|
52
CHANGES.rst
52
CHANGES.rst
|
@ -1,3 +1,55 @@
|
|||
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)
|
||||
--------------------------
|
||||
|
||||
|
|
|
@ -8,4 +8,7 @@ recursive-include docs *
|
|||
recursive-include requirements *
|
||||
recursive-include tests *
|
||||
recursive-include django_filters/locale *
|
||||
prune docs/_build
|
||||
recursive-include django_filters/templates *.html
|
||||
prune docs/_build
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
||||
|
|
11
Makefile
11
Makefile
|
@ -1,11 +0,0 @@
|
|||
.PHONY: deps, test, clean
|
||||
|
||||
deps:
|
||||
pip install -r ./requirements/test.txt
|
||||
|
||||
test:
|
||||
./runtests.py
|
||||
|
||||
clean:
|
||||
rm -r build dist django_filter.egg-info
|
||||
|
|
@ -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
|
52
README.rst
52
README.rst
|
@ -1,20 +1,27 @@
|
|||
Django Filter
|
||||
=============
|
||||
|
||||
Django-filter is a reusable Django application for allowing users to filter
|
||||
querysets dynamically.
|
||||
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.3, 3.4, 3.5
|
||||
* **Django**: 1.8, 1.9, 1.10
|
||||
* **DRF**: 3.4, 3.5
|
||||
* **Python**: 2.7, 3.4, 3.5, 3.6
|
||||
* **Django**: 1.8, 1.10, 1.11
|
||||
* **DRF**: 3.7
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -25,11 +32,15 @@ Install using pip:
|
|||
|
||||
pip install django-filter
|
||||
|
||||
Or clone the repo and add to your ``PYTHONPATH``:
|
||||
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||
|
||||
.. code-block:: sh
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
git clone git@github.com:carltongibson/django-filter.git
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
@ -57,22 +68,28 @@ And then in your view you could do:
|
|||
filter = ProductFilter(request.GET, queryset=Product.objects.all())
|
||||
return render(request, 'my_app/template.html', {'filter': filter})
|
||||
|
||||
Django-filters additionally supports specifying ``FilterSet`` fields using
|
||||
a dictionary to specify filters with lookup types:
|
||||
|
||||
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
|
||||
|
||||
import django_filters
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class ProductFilter(filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = {'name': ['exact', 'icontains'],
|
||||
'price': ['exact', 'gte', 'lte'],
|
||||
}
|
||||
fields = ('category', 'in_stock')
|
||||
|
||||
|
||||
For more details see the `DRF integration docs`_.
|
||||
|
||||
The filters will be available as ``'name'``, ``'name__icontains'``,
|
||||
``'price'``, ``'price__gte'``, and ``'price__lte'`` in the above example.
|
||||
|
||||
Support
|
||||
-------
|
||||
|
@ -82,3 +99,4 @@ If you have questions about usage or development you can join the
|
|||
|
||||
.. _`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
|
||||
|
|
|
@ -1,5 +1,146 @@
|
|||
django-filter (0.11.0-0) wheezy-eobuilder; urgency=low
|
||||
django-filter (1.1.0-1) unstable; urgency=medium
|
||||
|
||||
* Initial package
|
||||
* New upstream version 1.1.0
|
||||
* Refresh patches
|
||||
|
||||
-- Benjamin Dauvergne <bdauvergne@entrouvert.com> Thu, 12 Oct 2015 21:00:32 +0200
|
||||
-- Antonio Terceiro <terceiro@debian.org> Wed, 10 Jan 2018 10:50:46 -0200
|
||||
|
||||
django-filter (1.0.4-2) unstable; urgency=medium
|
||||
|
||||
* Drop transitional packages django-filter and python-django-filter.
|
||||
Closes: #878389.
|
||||
|
||||
-- Brian May <bam@debian.org> Tue, 31 Oct 2017 07:53:45 +1100
|
||||
|
||||
django-filter (1.0.4-1) unstable; urgency=medium
|
||||
|
||||
[ Brian May ]
|
||||
* Remove broken symlink. Closes: #857825.
|
||||
* Fix minor lintian warnings.
|
||||
* New upstream version. Closes: #865802, closes: #863024.
|
||||
|
||||
[ Michael Fladischer ]
|
||||
* Add python(3)-djangorestframework and python(3)-django-crispy-forms
|
||||
to Build-Depends so tests can run successfully.
|
||||
* Clean up modified files in docs/.build and django_filter.egg-info to
|
||||
allow two builds in a row.
|
||||
* Use https:// for copyright-format 1.0 URL.
|
||||
* Reformat packaging files with cme for better readability and remove
|
||||
unnecessary versioned dependencies.
|
||||
* Add unique parts to short and long descriptions.
|
||||
* Switch to python3-sphinx.
|
||||
* Rebuild all django.mo files from source using python3-babel.
|
||||
* Add patch to fix value of Language fields for django.po files.
|
||||
|
||||
-- Brian May <bam@debian.org> Sat, 29 Jul 2017 17:09:04 +1000
|
||||
|
||||
django-filter (0.13.0-1) unstable; urgency=medium
|
||||
|
||||
[ Ondřej Nový ]
|
||||
* Fixed homepage (https)
|
||||
|
||||
[ Brian May ]
|
||||
* New upstream version.
|
||||
|
||||
-- Brian May <bam@debian.org> Wed, 06 Apr 2016 12:39:17 +1000
|
||||
|
||||
django-filter (0.11.0-2) unstable; urgency=medium
|
||||
|
||||
* Run tests for all Python 3.*
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 26 Oct 2015 13:00:58 +1100
|
||||
|
||||
django-filter (0.11.0-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version.
|
||||
* Add fix_tests.patch, applied upstream see
|
||||
https://github.com/alex/django-filter/pull/287
|
||||
Closes: #796754: FTBFS: test_filtering_uses_distinct raises AttributeError
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 24 Aug 2015 11:52:41 +1000
|
||||
|
||||
django-filter (0.9.2-1) unstable; urgency=medium
|
||||
|
||||
* New upstream version.
|
||||
* Source package comes with prebuilt docs which include jquery.js and
|
||||
underscore.js, so provide sources to keep Lintian happy. Note none of these
|
||||
files are not used for the built Debian package.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 10 Apr 2015 10:38:43 +1000
|
||||
|
||||
django-filter (0.7-4) unstable; urgency=low
|
||||
|
||||
* Add patch to fix tests in Django 1.7. Closes: #755635.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 11 Aug 2014 10:05:52 +1000
|
||||
|
||||
django-filter (0.7-3) unstable; urgency=low
|
||||
|
||||
* Python3 package.
|
||||
|
||||
-- Brian May <bam@debian.org> Thu, 03 Jul 2014 13:55:03 +1000
|
||||
|
||||
django-filter (0.7-1) unstable; urgency=low
|
||||
|
||||
* New upstream version.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 04 Apr 2014 09:40:31 +1100
|
||||
|
||||
django-filter (0.6-3) unstable; urgency=low
|
||||
|
||||
* python-django-filters-doc now has have Breaks+Replaces:
|
||||
python-django-filter-doc (<< 0.6-2). Closes: #720046.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 02 Aug 2013 15:47:04 +1000
|
||||
|
||||
django-filter (0.6-2) unstable; urgency=low
|
||||
|
||||
* Rename python-django-filter to python-django-filters to make
|
||||
it compliant with Debian Python policy.
|
||||
* Provide transitional dummy package.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 02 Aug 2013 11:42:17 +1000
|
||||
|
||||
django-filter (0.6-1) unstable; urgency=low
|
||||
|
||||
[ Michael Fladischer ]
|
||||
* New upstream release.
|
||||
* Set DPMT as Uploaders.
|
||||
* Switch to dh_python2.
|
||||
* Bump Standards version to 3.9.4.
|
||||
* Bump debhelper Build-Depends to >= 8.1.0~.
|
||||
* Rename to python-django-filter, provide transitional dummy package
|
||||
* Build documentation with sphinx and move it to separate package
|
||||
python-django-filter-doc.
|
||||
* Drop patches.
|
||||
* Change short and long description.
|
||||
* Make dependencies on python-django versioned.
|
||||
* Drop debian/pycompat.
|
||||
* Add X-Python-Version.
|
||||
* Use DEP5 format for d/copyright.
|
||||
django-filter.
|
||||
* Add d/watch file.
|
||||
* Move Homepage field to source section.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 17 Jun 2013 13:53:11 +1000
|
||||
|
||||
django-filter (0.5.3-3) unstable; urgency=low
|
||||
|
||||
* Clean out debian/patches/debian-changes-0.5.3-2 and fix FTBS.
|
||||
Closes: #643092.
|
||||
* Increase standards version to 3.9.2 from 3.9.1
|
||||
* Add patch from upstream to make XMLField optional.
|
||||
|
||||
-- Brian May <bam@debian.org> Wed, 12 Oct 2011 09:50:46 +1100
|
||||
|
||||
django-filter (0.5.3-2) unstable; urgency=low
|
||||
|
||||
* Remove redundant line from debian/rules.
|
||||
|
||||
-- Brian May <bam@debian.org> Fri, 08 Apr 2011 10:41:00 +1000
|
||||
|
||||
django-filter (0.5.3-1) unstable; urgency=low
|
||||
|
||||
* Initial release. Closes: #619195.
|
||||
|
||||
-- Brian May <bam@debian.org> Mon, 28 Mar 2011 13:41:07 +1100
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
django_filter.egg-info/SOURCES.txt
|
||||
django_filters/locale/*/LC_MESSAGES/django.mo
|
|
@ -1 +1 @@
|
|||
7
|
||||
10
|
||||
|
|
|
@ -1,12 +1,73 @@
|
|||
Source: django-filter
|
||||
Maintainer: Entr'ouvert <info@entrouvert.com>
|
||||
Maintainer: Brian May <bam@debian.org>
|
||||
Uploaders: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: python-setuptools (>= 0.6b3), python3-setuptools, python-all (>= 2.6.6-3), python3-all, debhelper (>= 7)
|
||||
Standards-Version: 3.9.1
|
||||
X-Python-Version: >= 2.7
|
||||
Build-Depends: debhelper (>=10),
|
||||
dh-python,
|
||||
python-all,
|
||||
python-django,
|
||||
python-django-crispy-forms,
|
||||
python-djangorestframework,
|
||||
python-mock,
|
||||
python-setuptools,
|
||||
python3-all,
|
||||
python3-django,
|
||||
python3-django-crispy-forms,
|
||||
python3-djangorestframework,
|
||||
python3-mock,
|
||||
python3-setuptools,
|
||||
python3-sphinx-rtd-theme
|
||||
Build-Depends-Indep: libjs-jquery,
|
||||
python3-babel,
|
||||
python3-sphinx
|
||||
Standards-Version: 4.0.0
|
||||
Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/django-filter.git
|
||||
Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/django-filter.git
|
||||
Homepage: https://github.com/alex/django-filter
|
||||
X-Python-Version: >= 2.6
|
||||
|
||||
Package: python-django-filters
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends}, python-django (>= 1.4.5)
|
||||
Description: Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
|
||||
Depends: python-django,
|
||||
${misc:Depends},
|
||||
${python:Depends}
|
||||
Suggests: python-django-filters-doc
|
||||
Breaks: django-filter (<< 0.6),
|
||||
python-django-filter (<< 0.6-2)
|
||||
Replaces: django-filter (<< 0.6),
|
||||
python-django-filter (<< 0.6-2)
|
||||
Description: filter Django QuerySets based on user selections (Python2 version)
|
||||
Django-filter is a generic, reusable application to alleviate some of the more
|
||||
mundane bits of view code. Specifically allowing the users to filter down a
|
||||
queryset based on a model’s fields and displaying the form to let them do this.
|
||||
.
|
||||
This package contains the Python 2 version of the library.
|
||||
|
||||
|
||||
Package: python3-django-filters
|
||||
Architecture: all
|
||||
Depends: python3-django,
|
||||
${misc:Depends},
|
||||
${python3:Depends}
|
||||
Suggests: python-django-filters-doc
|
||||
Description: filter Django QuerySets based on user selections (Python3 version)
|
||||
Django-filter is a generic, reusable application to alleviate some of the more
|
||||
mundane bits of view code. Specifically allowing the users to filter down a
|
||||
queryset based on a model’s fields and displaying the form to let them do this.
|
||||
.
|
||||
This package contains the Python 3 version of the library.
|
||||
|
||||
Package: python-django-filters-doc
|
||||
Architecture: all
|
||||
Section: doc
|
||||
Depends: ${misc:Depends},
|
||||
${sphinxdoc:Depends}
|
||||
Breaks: python-django-filter-doc (<< 0.6-2)
|
||||
Replaces: python-django-filter-doc (<< 0.6-2)
|
||||
Description: filter Django QuerySets based on user selections (Documentation)
|
||||
Django-filter is a generic, reusable application to alleviate some of the more
|
||||
mundane bits of view code. Specifically allowing the users to filter down a
|
||||
queryset based on a model’s fields and displaying the form to let them do this.
|
||||
.
|
||||
This package contains the documentation.
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: django-filter
|
||||
Upstream-Contact: Alex Gaynor <alex.gaynor@gmail.com>
|
||||
Source: https://pypi.python.org/pypi/django-filter
|
||||
|
||||
Files: *
|
||||
Copyright: 2009, Alex Gaynor <alex.gaynor@gmail.com>
|
||||
License: BSD-django-filter
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2011, Brian May <bam@debian.org>
|
||||
2013, Fladischer Michael <FladischerMichael@fladi.at>
|
||||
License: BSD-django-filter
|
||||
|
||||
License: BSD-django-filter
|
||||
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.
|
|
@ -0,0 +1,2 @@
|
|||
[DEFAULT]
|
||||
debian-branch=debian/master
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,999 @@
|
|||
// Underscore.js 1.3.1
|
||||
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||
// Underscore is freely distributable under the MIT license.
|
||||
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||
// For all details and documentation:
|
||||
// http://documentcloud.github.com/underscore
|
||||
|
||||
(function() {
|
||||
|
||||
// Baseline setup
|
||||
// --------------
|
||||
|
||||
// Establish the root object, `window` in the browser, or `global` on the server.
|
||||
var root = this;
|
||||
|
||||
// Save the previous value of the `_` variable.
|
||||
var previousUnderscore = root._;
|
||||
|
||||
// Establish the object that gets returned to break out of a loop iteration.
|
||||
var breaker = {};
|
||||
|
||||
// Save bytes in the minified (but not gzipped) version:
|
||||
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
|
||||
|
||||
// Create quick reference variables for speed access to core prototypes.
|
||||
var slice = ArrayProto.slice,
|
||||
unshift = ArrayProto.unshift,
|
||||
toString = ObjProto.toString,
|
||||
hasOwnProperty = ObjProto.hasOwnProperty;
|
||||
|
||||
// All **ECMAScript 5** native function implementations that we hope to use
|
||||
// are declared here.
|
||||
var
|
||||
nativeForEach = ArrayProto.forEach,
|
||||
nativeMap = ArrayProto.map,
|
||||
nativeReduce = ArrayProto.reduce,
|
||||
nativeReduceRight = ArrayProto.reduceRight,
|
||||
nativeFilter = ArrayProto.filter,
|
||||
nativeEvery = ArrayProto.every,
|
||||
nativeSome = ArrayProto.some,
|
||||
nativeIndexOf = ArrayProto.indexOf,
|
||||
nativeLastIndexOf = ArrayProto.lastIndexOf,
|
||||
nativeIsArray = Array.isArray,
|
||||
nativeKeys = Object.keys,
|
||||
nativeBind = FuncProto.bind;
|
||||
|
||||
// Create a safe reference to the Underscore object for use below.
|
||||
var _ = function(obj) { return new wrapper(obj); };
|
||||
|
||||
// Export the Underscore object for **Node.js**, with
|
||||
// backwards-compatibility for the old `require()` API. If we're in
|
||||
// the browser, add `_` as a global object via a string identifier,
|
||||
// for Closure Compiler "advanced" mode.
|
||||
if (typeof exports !== 'undefined') {
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
exports = module.exports = _;
|
||||
}
|
||||
exports._ = _;
|
||||
} else {
|
||||
root['_'] = _;
|
||||
}
|
||||
|
||||
// Current version.
|
||||
_.VERSION = '1.3.1';
|
||||
|
||||
// Collection Functions
|
||||
// --------------------
|
||||
|
||||
// The cornerstone, an `each` implementation, aka `forEach`.
|
||||
// Handles objects with the built-in `forEach`, arrays, and raw objects.
|
||||
// Delegates to **ECMAScript 5**'s native `forEach` if available.
|
||||
var each = _.each = _.forEach = function(obj, iterator, context) {
|
||||
if (obj == null) return;
|
||||
if (nativeForEach && obj.forEach === nativeForEach) {
|
||||
obj.forEach(iterator, context);
|
||||
} else if (obj.length === +obj.length) {
|
||||
for (var i = 0, l = obj.length; i < l; i++) {
|
||||
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
|
||||
}
|
||||
} else {
|
||||
for (var key in obj) {
|
||||
if (_.has(obj, key)) {
|
||||
if (iterator.call(context, obj[key], key, obj) === breaker) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Return the results of applying the iterator to each element.
|
||||
// Delegates to **ECMAScript 5**'s native `map` if available.
|
||||
_.map = _.collect = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
results[results.length] = iterator.call(context, value, index, list);
|
||||
});
|
||||
if (obj.length === +obj.length) results.length = obj.length;
|
||||
return results;
|
||||
};
|
||||
|
||||
// **Reduce** builds up a single result from a list of values, aka `inject`,
|
||||
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
|
||||
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduce && obj.reduce === nativeReduce) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
|
||||
}
|
||||
each(obj, function(value, index, list) {
|
||||
if (!initial) {
|
||||
memo = value;
|
||||
initial = true;
|
||||
} else {
|
||||
memo = iterator.call(context, memo, value, index, list);
|
||||
}
|
||||
});
|
||||
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
|
||||
return memo;
|
||||
};
|
||||
|
||||
// The right-associative version of reduce, also known as `foldr`.
|
||||
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
|
||||
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
|
||||
var initial = arguments.length > 2;
|
||||
if (obj == null) obj = [];
|
||||
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
|
||||
if (context) iterator = _.bind(iterator, context);
|
||||
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
|
||||
}
|
||||
var reversed = _.toArray(obj).reverse();
|
||||
if (context && !initial) iterator = _.bind(iterator, context);
|
||||
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
|
||||
};
|
||||
|
||||
// Return the first value which passes a truth test. Aliased as `detect`.
|
||||
_.find = _.detect = function(obj, iterator, context) {
|
||||
var result;
|
||||
any(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Return all the elements that pass a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `filter` if available.
|
||||
// Aliased as `select`.
|
||||
_.filter = _.select = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Return all the elements for which a truth test fails.
|
||||
_.reject = function(obj, iterator, context) {
|
||||
var results = [];
|
||||
if (obj == null) return results;
|
||||
each(obj, function(value, index, list) {
|
||||
if (!iterator.call(context, value, index, list)) results[results.length] = value;
|
||||
});
|
||||
return results;
|
||||
};
|
||||
|
||||
// Determine whether all of the elements match a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `every` if available.
|
||||
// Aliased as `all`.
|
||||
_.every = _.all = function(obj, iterator, context) {
|
||||
var result = true;
|
||||
if (obj == null) return result;
|
||||
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Determine if at least one element in the object matches a truth test.
|
||||
// Delegates to **ECMAScript 5**'s native `some` if available.
|
||||
// Aliased as `any`.
|
||||
var any = _.some = _.any = function(obj, iterator, context) {
|
||||
iterator || (iterator = _.identity);
|
||||
var result = false;
|
||||
if (obj == null) return result;
|
||||
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
|
||||
each(obj, function(value, index, list) {
|
||||
if (result || (result = iterator.call(context, value, index, list))) return breaker;
|
||||
});
|
||||
return !!result;
|
||||
};
|
||||
|
||||
// Determine if a given value is included in the array or object using `===`.
|
||||
// Aliased as `contains`.
|
||||
_.include = _.contains = function(obj, target) {
|
||||
var found = false;
|
||||
if (obj == null) return found;
|
||||
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
|
||||
found = any(obj, function(value) {
|
||||
return value === target;
|
||||
});
|
||||
return found;
|
||||
};
|
||||
|
||||
// Invoke a method (with arguments) on every item in a collection.
|
||||
_.invoke = function(obj, method) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return _.map(obj, function(value) {
|
||||
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
|
||||
});
|
||||
};
|
||||
|
||||
// Convenience version of a common use case of `map`: fetching a property.
|
||||
_.pluck = function(obj, key) {
|
||||
return _.map(obj, function(value){ return value[key]; });
|
||||
};
|
||||
|
||||
// Return the maximum element or (element-based computation).
|
||||
_.max = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return -Infinity;
|
||||
var result = {computed : -Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed >= result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Return the minimum element (or element-based computation).
|
||||
_.min = function(obj, iterator, context) {
|
||||
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
|
||||
if (!iterator && _.isEmpty(obj)) return Infinity;
|
||||
var result = {computed : Infinity};
|
||||
each(obj, function(value, index, list) {
|
||||
var computed = iterator ? iterator.call(context, value, index, list) : value;
|
||||
computed < result.computed && (result = {value : value, computed : computed});
|
||||
});
|
||||
return result.value;
|
||||
};
|
||||
|
||||
// Shuffle an array.
|
||||
_.shuffle = function(obj) {
|
||||
var shuffled = [], rand;
|
||||
each(obj, function(value, index, list) {
|
||||
if (index == 0) {
|
||||
shuffled[0] = value;
|
||||
} else {
|
||||
rand = Math.floor(Math.random() * (index + 1));
|
||||
shuffled[index] = shuffled[rand];
|
||||
shuffled[rand] = value;
|
||||
}
|
||||
});
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
// Sort the object's values by a criterion produced by an iterator.
|
||||
_.sortBy = function(obj, iterator, context) {
|
||||
return _.pluck(_.map(obj, function(value, index, list) {
|
||||
return {
|
||||
value : value,
|
||||
criteria : iterator.call(context, value, index, list)
|
||||
};
|
||||
}).sort(function(left, right) {
|
||||
var a = left.criteria, b = right.criteria;
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}), 'value');
|
||||
};
|
||||
|
||||
// Groups the object's values by a criterion. Pass either a string attribute
|
||||
// to group by, or a function that returns the criterion.
|
||||
_.groupBy = function(obj, val) {
|
||||
var result = {};
|
||||
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
|
||||
each(obj, function(value, index) {
|
||||
var key = iterator(value, index);
|
||||
(result[key] || (result[key] = [])).push(value);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// Use a comparator function to figure out at what index an object should
|
||||
// be inserted so as to maintain order. Uses binary search.
|
||||
_.sortedIndex = function(array, obj, iterator) {
|
||||
iterator || (iterator = _.identity);
|
||||
var low = 0, high = array.length;
|
||||
while (low < high) {
|
||||
var mid = (low + high) >> 1;
|
||||
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
|
||||
}
|
||||
return low;
|
||||
};
|
||||
|
||||
// Safely convert anything iterable into a real, live array.
|
||||
_.toArray = function(iterable) {
|
||||
if (!iterable) return [];
|
||||
if (iterable.toArray) return iterable.toArray();
|
||||
if (_.isArray(iterable)) return slice.call(iterable);
|
||||
if (_.isArguments(iterable)) return slice.call(iterable);
|
||||
return _.values(iterable);
|
||||
};
|
||||
|
||||
// Return the number of elements in an object.
|
||||
_.size = function(obj) {
|
||||
return _.toArray(obj).length;
|
||||
};
|
||||
|
||||
// Array Functions
|
||||
// ---------------
|
||||
|
||||
// Get the first element of an array. Passing **n** will return the first N
|
||||
// values in the array. Aliased as `head`. The **guard** check allows it to work
|
||||
// with `_.map`.
|
||||
_.first = _.head = function(array, n, guard) {
|
||||
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
|
||||
};
|
||||
|
||||
// Returns everything but the last entry of the array. Especcialy useful on
|
||||
// the arguments object. Passing **n** will return all the values in
|
||||
// the array, excluding the last N. The **guard** check allows it to work with
|
||||
// `_.map`.
|
||||
_.initial = function(array, n, guard) {
|
||||
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
|
||||
};
|
||||
|
||||
// Get the last element of an array. Passing **n** will return the last N
|
||||
// values in the array. The **guard** check allows it to work with `_.map`.
|
||||
_.last = function(array, n, guard) {
|
||||
if ((n != null) && !guard) {
|
||||
return slice.call(array, Math.max(array.length - n, 0));
|
||||
} else {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
};
|
||||
|
||||
// Returns everything but the first entry of the array. Aliased as `tail`.
|
||||
// Especially useful on the arguments object. Passing an **index** will return
|
||||
// the rest of the values in the array from that index onward. The **guard**
|
||||
// check allows it to work with `_.map`.
|
||||
_.rest = _.tail = function(array, index, guard) {
|
||||
return slice.call(array, (index == null) || guard ? 1 : index);
|
||||
};
|
||||
|
||||
// Trim out all falsy values from an array.
|
||||
_.compact = function(array) {
|
||||
return _.filter(array, function(value){ return !!value; });
|
||||
};
|
||||
|
||||
// Return a completely flattened version of an array.
|
||||
_.flatten = function(array, shallow) {
|
||||
return _.reduce(array, function(memo, value) {
|
||||
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
|
||||
memo[memo.length] = value;
|
||||
return memo;
|
||||
}, []);
|
||||
};
|
||||
|
||||
// Return a version of the array that does not contain the specified value(s).
|
||||
_.without = function(array) {
|
||||
return _.difference(array, slice.call(arguments, 1));
|
||||
};
|
||||
|
||||
// Produce a duplicate-free version of the array. If the array has already
|
||||
// been sorted, you have the option of using a faster algorithm.
|
||||
// Aliased as `unique`.
|
||||
_.uniq = _.unique = function(array, isSorted, iterator) {
|
||||
var initial = iterator ? _.map(array, iterator) : array;
|
||||
var result = [];
|
||||
_.reduce(initial, function(memo, el, i) {
|
||||
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
|
||||
memo[memo.length] = el;
|
||||
result[result.length] = array[i];
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Produce an array that contains the union: each distinct element from all of
|
||||
// the passed-in arrays.
|
||||
_.union = function() {
|
||||
return _.uniq(_.flatten(arguments, true));
|
||||
};
|
||||
|
||||
// Produce an array that contains every item shared between all the
|
||||
// passed-in arrays. (Aliased as "intersect" for back-compat.)
|
||||
_.intersection = _.intersect = function(array) {
|
||||
var rest = slice.call(arguments, 1);
|
||||
return _.filter(_.uniq(array), function(item) {
|
||||
return _.every(rest, function(other) {
|
||||
return _.indexOf(other, item) >= 0;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Take the difference between one array and a number of other arrays.
|
||||
// Only the elements present in just the first array will remain.
|
||||
_.difference = function(array) {
|
||||
var rest = _.flatten(slice.call(arguments, 1));
|
||||
return _.filter(array, function(value){ return !_.include(rest, value); });
|
||||
};
|
||||
|
||||
// Zip together multiple lists into a single array -- elements that share
|
||||
// an index go together.
|
||||
_.zip = function() {
|
||||
var args = slice.call(arguments);
|
||||
var length = _.max(_.pluck(args, 'length'));
|
||||
var results = new Array(length);
|
||||
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
|
||||
return results;
|
||||
};
|
||||
|
||||
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
|
||||
// we need this function. Return the position of the first occurrence of an
|
||||
// item in an array, or -1 if the item is not included in the array.
|
||||
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
|
||||
// If the array is large and already in sort order, pass `true`
|
||||
// for **isSorted** to use binary search.
|
||||
_.indexOf = function(array, item, isSorted) {
|
||||
if (array == null) return -1;
|
||||
var i, l;
|
||||
if (isSorted) {
|
||||
i = _.sortedIndex(array, item);
|
||||
return array[i] === item ? i : -1;
|
||||
}
|
||||
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
|
||||
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
|
||||
_.lastIndexOf = function(array, item) {
|
||||
if (array == null) return -1;
|
||||
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
|
||||
var i = array.length;
|
||||
while (i--) if (i in array && array[i] === item) return i;
|
||||
return -1;
|
||||
};
|
||||
|
||||
// Generate an integer Array containing an arithmetic progression. A port of
|
||||
// the native Python `range()` function. See
|
||||
// [the Python documentation](http://docs.python.org/library/functions.html#range).
|
||||
_.range = function(start, stop, step) {
|
||||
if (arguments.length <= 1) {
|
||||
stop = start || 0;
|
||||
start = 0;
|
||||
}
|
||||
step = arguments[2] || 1;
|
||||
|
||||
var len = Math.max(Math.ceil((stop - start) / step), 0);
|
||||
var idx = 0;
|
||||
var range = new Array(len);
|
||||
|
||||
while(idx < len) {
|
||||
range[idx++] = start;
|
||||
start += step;
|
||||
}
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
// Function (ahem) Functions
|
||||
// ------------------
|
||||
|
||||
// Reusable constructor function for prototype setting.
|
||||
var ctor = function(){};
|
||||
|
||||
// Create a function bound to a given object (assigning `this`, and arguments,
|
||||
// optionally). Binding with arguments is also known as `curry`.
|
||||
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
|
||||
// We check for `func.bind` first, to fail fast when `func` is undefined.
|
||||
_.bind = function bind(func, context) {
|
||||
var bound, args;
|
||||
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
|
||||
if (!_.isFunction(func)) throw new TypeError;
|
||||
args = slice.call(arguments, 2);
|
||||
return bound = function() {
|
||||
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
|
||||
ctor.prototype = func.prototype;
|
||||
var self = new ctor;
|
||||
var result = func.apply(self, args.concat(slice.call(arguments)));
|
||||
if (Object(result) === result) return result;
|
||||
return self;
|
||||
};
|
||||
};
|
||||
|
||||
// Bind all of an object's methods to that object. Useful for ensuring that
|
||||
// all callbacks defined on an object belong to it.
|
||||
_.bindAll = function(obj) {
|
||||
var funcs = slice.call(arguments, 1);
|
||||
if (funcs.length == 0) funcs = _.functions(obj);
|
||||
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Memoize an expensive function by storing its results.
|
||||
_.memoize = function(func, hasher) {
|
||||
var memo = {};
|
||||
hasher || (hasher = _.identity);
|
||||
return function() {
|
||||
var key = hasher.apply(this, arguments);
|
||||
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
|
||||
};
|
||||
};
|
||||
|
||||
// Delays a function for the given number of milliseconds, and then calls
|
||||
// it with the arguments supplied.
|
||||
_.delay = function(func, wait) {
|
||||
var args = slice.call(arguments, 2);
|
||||
return setTimeout(function(){ return func.apply(func, args); }, wait);
|
||||
};
|
||||
|
||||
// Defers a function, scheduling it to run after the current call stack has
|
||||
// cleared.
|
||||
_.defer = function(func) {
|
||||
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
|
||||
};
|
||||
|
||||
// Returns a function, that, when invoked, will only be triggered at most once
|
||||
// during a given window of time.
|
||||
_.throttle = function(func, wait) {
|
||||
var context, args, timeout, throttling, more;
|
||||
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
|
||||
return function() {
|
||||
context = this; args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (more) func.apply(context, args);
|
||||
whenDone();
|
||||
};
|
||||
if (!timeout) timeout = setTimeout(later, wait);
|
||||
if (throttling) {
|
||||
more = true;
|
||||
} else {
|
||||
func.apply(context, args);
|
||||
}
|
||||
whenDone();
|
||||
throttling = true;
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds.
|
||||
_.debounce = function(func, wait) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this, args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will be executed at most one time, no matter how
|
||||
// often you call it. Useful for lazy initialization.
|
||||
_.once = function(func) {
|
||||
var ran = false, memo;
|
||||
return function() {
|
||||
if (ran) return memo;
|
||||
ran = true;
|
||||
return memo = func.apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns the first function passed as an argument to the second,
|
||||
// allowing you to adjust arguments, run code before and after, and
|
||||
// conditionally execute the original function.
|
||||
_.wrap = function(func, wrapper) {
|
||||
return function() {
|
||||
var args = [func].concat(slice.call(arguments, 0));
|
||||
return wrapper.apply(this, args);
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that is the composition of a list of functions, each
|
||||
// consuming the return value of the function that follows.
|
||||
_.compose = function() {
|
||||
var funcs = arguments;
|
||||
return function() {
|
||||
var args = arguments;
|
||||
for (var i = funcs.length - 1; i >= 0; i--) {
|
||||
args = [funcs[i].apply(this, args)];
|
||||
}
|
||||
return args[0];
|
||||
};
|
||||
};
|
||||
|
||||
// Returns a function that will only be executed after being called N times.
|
||||
_.after = function(times, func) {
|
||||
if (times <= 0) return func();
|
||||
return function() {
|
||||
if (--times < 1) { return func.apply(this, arguments); }
|
||||
};
|
||||
};
|
||||
|
||||
// Object Functions
|
||||
// ----------------
|
||||
|
||||
// Retrieve the names of an object's properties.
|
||||
// Delegates to **ECMAScript 5**'s native `Object.keys`
|
||||
_.keys = nativeKeys || function(obj) {
|
||||
if (obj !== Object(obj)) throw new TypeError('Invalid object');
|
||||
var keys = [];
|
||||
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
|
||||
return keys;
|
||||
};
|
||||
|
||||
// Retrieve the values of an object's properties.
|
||||
_.values = function(obj) {
|
||||
return _.map(obj, _.identity);
|
||||
};
|
||||
|
||||
// Return a sorted list of the function names available on the object.
|
||||
// Aliased as `methods`
|
||||
_.functions = _.methods = function(obj) {
|
||||
var names = [];
|
||||
for (var key in obj) {
|
||||
if (_.isFunction(obj[key])) names.push(key);
|
||||
}
|
||||
return names.sort();
|
||||
};
|
||||
|
||||
// Extend a given object with all the properties in passed-in object(s).
|
||||
_.extend = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Fill in a given object with default properties.
|
||||
_.defaults = function(obj) {
|
||||
each(slice.call(arguments, 1), function(source) {
|
||||
for (var prop in source) {
|
||||
if (obj[prop] == null) obj[prop] = source[prop];
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Create a (shallow-cloned) duplicate of an object.
|
||||
_.clone = function(obj) {
|
||||
if (!_.isObject(obj)) return obj;
|
||||
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
|
||||
};
|
||||
|
||||
// Invokes interceptor with the obj, and then returns obj.
|
||||
// The primary purpose of this method is to "tap into" a method chain, in
|
||||
// order to perform operations on intermediate results within the chain.
|
||||
_.tap = function(obj, interceptor) {
|
||||
interceptor(obj);
|
||||
return obj;
|
||||
};
|
||||
|
||||
// Internal recursive comparison function.
|
||||
function eq(a, b, stack) {
|
||||
// Identical objects are equal. `0 === -0`, but they aren't identical.
|
||||
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
|
||||
if (a === b) return a !== 0 || 1 / a == 1 / b;
|
||||
// A strict comparison is necessary because `null == undefined`.
|
||||
if (a == null || b == null) return a === b;
|
||||
// Unwrap any wrapped objects.
|
||||
if (a._chain) a = a._wrapped;
|
||||
if (b._chain) b = b._wrapped;
|
||||
// Invoke a custom `isEqual` method if one is provided.
|
||||
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
|
||||
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
|
||||
// Compare `[[Class]]` names.
|
||||
var className = toString.call(a);
|
||||
if (className != toString.call(b)) return false;
|
||||
switch (className) {
|
||||
// Strings, numbers, dates, and booleans are compared by value.
|
||||
case '[object String]':
|
||||
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
|
||||
// equivalent to `new String("5")`.
|
||||
return a == String(b);
|
||||
case '[object Number]':
|
||||
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
|
||||
// other numeric values.
|
||||
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
|
||||
case '[object Date]':
|
||||
case '[object Boolean]':
|
||||
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
|
||||
// millisecond representations. Note that invalid dates with millisecond representations
|
||||
// of `NaN` are not equivalent.
|
||||
return +a == +b;
|
||||
// RegExps are compared by their source patterns and flags.
|
||||
case '[object RegExp]':
|
||||
return a.source == b.source &&
|
||||
a.global == b.global &&
|
||||
a.multiline == b.multiline &&
|
||||
a.ignoreCase == b.ignoreCase;
|
||||
}
|
||||
if (typeof a != 'object' || typeof b != 'object') return false;
|
||||
// Assume equality for cyclic structures. The algorithm for detecting cyclic
|
||||
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
|
||||
var length = stack.length;
|
||||
while (length--) {
|
||||
// Linear search. Performance is inversely proportional to the number of
|
||||
// unique nested structures.
|
||||
if (stack[length] == a) return true;
|
||||
}
|
||||
// Add the first object to the stack of traversed objects.
|
||||
stack.push(a);
|
||||
var size = 0, result = true;
|
||||
// Recursively compare objects and arrays.
|
||||
if (className == '[object Array]') {
|
||||
// Compare array lengths to determine if a deep comparison is necessary.
|
||||
size = a.length;
|
||||
result = size == b.length;
|
||||
if (result) {
|
||||
// Deep compare the contents, ignoring non-numeric properties.
|
||||
while (size--) {
|
||||
// Ensure commutative equality for sparse arrays.
|
||||
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Objects with different constructors are not equivalent.
|
||||
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
|
||||
// Deep compare objects.
|
||||
for (var key in a) {
|
||||
if (_.has(a, key)) {
|
||||
// Count the expected number of properties.
|
||||
size++;
|
||||
// Deep compare each member.
|
||||
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
|
||||
}
|
||||
}
|
||||
// Ensure that both objects contain the same number of properties.
|
||||
if (result) {
|
||||
for (key in b) {
|
||||
if (_.has(b, key) && !(size--)) break;
|
||||
}
|
||||
result = !size;
|
||||
}
|
||||
}
|
||||
// Remove the first object from the stack of traversed objects.
|
||||
stack.pop();
|
||||
return result;
|
||||
}
|
||||
|
||||
// Perform a deep comparison to check if two objects are equal.
|
||||
_.isEqual = function(a, b) {
|
||||
return eq(a, b, []);
|
||||
};
|
||||
|
||||
// Is a given array, string, or object empty?
|
||||
// An "empty" object has no enumerable own-properties.
|
||||
_.isEmpty = function(obj) {
|
||||
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
|
||||
for (var key in obj) if (_.has(obj, key)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Is a given value a DOM element?
|
||||
_.isElement = function(obj) {
|
||||
return !!(obj && obj.nodeType == 1);
|
||||
};
|
||||
|
||||
// Is a given value an array?
|
||||
// Delegates to ECMA5's native Array.isArray
|
||||
_.isArray = nativeIsArray || function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
|
||||
// Is a given variable an object?
|
||||
_.isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
|
||||
// Is a given variable an arguments object?
|
||||
_.isArguments = function(obj) {
|
||||
return toString.call(obj) == '[object Arguments]';
|
||||
};
|
||||
if (!_.isArguments(arguments)) {
|
||||
_.isArguments = function(obj) {
|
||||
return !!(obj && _.has(obj, 'callee'));
|
||||
};
|
||||
}
|
||||
|
||||
// Is a given value a function?
|
||||
_.isFunction = function(obj) {
|
||||
return toString.call(obj) == '[object Function]';
|
||||
};
|
||||
|
||||
// Is a given value a string?
|
||||
_.isString = function(obj) {
|
||||
return toString.call(obj) == '[object String]';
|
||||
};
|
||||
|
||||
// Is a given value a number?
|
||||
_.isNumber = function(obj) {
|
||||
return toString.call(obj) == '[object Number]';
|
||||
};
|
||||
|
||||
// Is the given value `NaN`?
|
||||
_.isNaN = function(obj) {
|
||||
// `NaN` is the only value for which `===` is not reflexive.
|
||||
return obj !== obj;
|
||||
};
|
||||
|
||||
// Is a given value a boolean?
|
||||
_.isBoolean = function(obj) {
|
||||
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
// Is a given value a date?
|
||||
_.isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
|
||||
// Is the given value a regular expression?
|
||||
_.isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
|
||||
// Is a given value equal to null?
|
||||
_.isNull = function(obj) {
|
||||
return obj === null;
|
||||
};
|
||||
|
||||
// Is a given variable undefined?
|
||||
_.isUndefined = function(obj) {
|
||||
return obj === void 0;
|
||||
};
|
||||
|
||||
// Has own property?
|
||||
_.has = function(obj, key) {
|
||||
return hasOwnProperty.call(obj, key);
|
||||
};
|
||||
|
||||
// Utility Functions
|
||||
// -----------------
|
||||
|
||||
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
|
||||
// previous owner. Returns a reference to the Underscore object.
|
||||
_.noConflict = function() {
|
||||
root._ = previousUnderscore;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Keep the identity function around for default iterators.
|
||||
_.identity = function(value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
// Run a function **n** times.
|
||||
_.times = function (n, iterator, context) {
|
||||
for (var i = 0; i < n; i++) iterator.call(context, i);
|
||||
};
|
||||
|
||||
// Escape a string for HTML interpolation.
|
||||
_.escape = function(string) {
|
||||
return (''+string).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/');
|
||||
};
|
||||
|
||||
// Add your own custom functions to the Underscore object, ensuring that
|
||||
// they're correctly added to the OOP wrapper as well.
|
||||
_.mixin = function(obj) {
|
||||
each(_.functions(obj), function(name){
|
||||
addToWrapper(name, _[name] = obj[name]);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate a unique integer id (unique within the entire client session).
|
||||
// Useful for temporary DOM ids.
|
||||
var idCounter = 0;
|
||||
_.uniqueId = function(prefix) {
|
||||
var id = idCounter++;
|
||||
return prefix ? prefix + id : id;
|
||||
};
|
||||
|
||||
// By default, Underscore uses ERB-style template delimiters, change the
|
||||
// following template settings to use alternative delimiters.
|
||||
_.templateSettings = {
|
||||
evaluate : /<%([\s\S]+?)%>/g,
|
||||
interpolate : /<%=([\s\S]+?)%>/g,
|
||||
escape : /<%-([\s\S]+?)%>/g
|
||||
};
|
||||
|
||||
// When customizing `templateSettings`, if you don't want to define an
|
||||
// interpolation, evaluation or escaping regex, we need one that is
|
||||
// guaranteed not to match.
|
||||
var noMatch = /.^/;
|
||||
|
||||
// Within an interpolation, evaluation, or escaping, remove HTML escaping
|
||||
// that had been previously added.
|
||||
var unescape = function(code) {
|
||||
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
|
||||
};
|
||||
|
||||
// JavaScript micro-templating, similar to John Resig's implementation.
|
||||
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
||||
// and correctly escapes quotes within interpolated code.
|
||||
_.template = function(str, data) {
|
||||
var c = _.templateSettings;
|
||||
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
|
||||
'with(obj||{}){__p.push(\'' +
|
||||
str.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(c.escape || noMatch, function(match, code) {
|
||||
return "',_.escape(" + unescape(code) + "),'";
|
||||
})
|
||||
.replace(c.interpolate || noMatch, function(match, code) {
|
||||
return "'," + unescape(code) + ",'";
|
||||
})
|
||||
.replace(c.evaluate || noMatch, function(match, code) {
|
||||
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
|
||||
})
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\t/g, '\\t')
|
||||
+ "');}return __p.join('');";
|
||||
var func = new Function('obj', '_', tmpl);
|
||||
if (data) return func(data, _);
|
||||
return function(data) {
|
||||
return func.call(this, data, _);
|
||||
};
|
||||
};
|
||||
|
||||
// Add a "chain" function, which will delegate to the wrapper.
|
||||
_.chain = function(obj) {
|
||||
return _(obj).chain();
|
||||
};
|
||||
|
||||
// The OOP Wrapper
|
||||
// ---------------
|
||||
|
||||
// If Underscore is called as a function, it returns a wrapped object that
|
||||
// can be used OO-style. This wrapper holds altered versions of all the
|
||||
// underscore functions. Wrapped objects may be chained.
|
||||
var wrapper = function(obj) { this._wrapped = obj; };
|
||||
|
||||
// Expose `wrapper.prototype` as `_.prototype`
|
||||
_.prototype = wrapper.prototype;
|
||||
|
||||
// Helper function to continue chaining intermediate results.
|
||||
var result = function(obj, chain) {
|
||||
return chain ? _(obj).chain() : obj;
|
||||
};
|
||||
|
||||
// A method to easily add functions to the OOP wrapper.
|
||||
var addToWrapper = function(name, func) {
|
||||
wrapper.prototype[name] = function() {
|
||||
var args = slice.call(arguments);
|
||||
unshift.call(args, this._wrapped);
|
||||
return result(func.apply(_, args), this._chain);
|
||||
};
|
||||
};
|
||||
|
||||
// Add all of the Underscore functions to the wrapper object.
|
||||
_.mixin(_);
|
||||
|
||||
// Add all mutator Array functions to the wrapper.
|
||||
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
var wrapped = this._wrapped;
|
||||
method.apply(wrapped, arguments);
|
||||
var length = wrapped.length;
|
||||
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
|
||||
return result(wrapped, this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Add all accessor Array functions to the wrapper.
|
||||
each(['concat', 'join', 'slice'], function(name) {
|
||||
var method = ArrayProto[name];
|
||||
wrapper.prototype[name] = function() {
|
||||
return result(method.apply(this._wrapped, arguments), this._chain);
|
||||
};
|
||||
});
|
||||
|
||||
// Start chaining a wrapped Underscore object.
|
||||
wrapper.prototype.chain = function() {
|
||||
this._chain = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
// Extracts the result from a wrapped and chained object.
|
||||
wrapper.prototype.value = function() {
|
||||
return this._wrapped;
|
||||
};
|
||||
|
||||
}).call(this);
|
|
@ -0,0 +1,32 @@
|
|||
From: Michael Fladischer <FladischerMichael@fladi.at>
|
||||
Date: Tue, 25 Jul 2017 22:06:20 +0200
|
||||
Subject: Fix value of Language fields for django.po files.
|
||||
|
||||
---
|
||||
django_filters/locale/fr/LC_MESSAGES/django.po | 2 +-
|
||||
django_filters/locale/pl/LC_MESSAGES/django.po | 2 +-
|
||||
django_filters/locale/zh_CN/LC_MESSAGES/django.po | 2 +-
|
||||
3 files changed, 3 insertions(+), 3 deletions(-)
|
||||
|
||||
--- a/django_filters/locale/fr/LC_MESSAGES/django.po
|
||||
+++ b/django_filters/locale/fr/LC_MESSAGES/django.po
|
||||
@@ -12,7 +12,7 @@ msgstr ""
|
||||
"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"
|
||||
+"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
--- a/django_filters/locale/zh_CN/LC_MESSAGES/django.po
|
||||
+++ b/django_filters/locale/zh_CN/LC_MESSAGES/django.po
|
||||
@@ -12,7 +12,7 @@ msgstr ""
|
||||
"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"
|
||||
+"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
|
@ -0,0 +1 @@
|
|||
2
|
|
@ -0,0 +1,9 @@
|
|||
Document: python-django-filter
|
||||
Title: Python django-filter Documentation
|
||||
Author: Alex Gaynor
|
||||
Abstract: This documentation gives an introduction to django-filter.
|
||||
Section: Programming/Python
|
||||
|
||||
Format: HTML
|
||||
Index: /usr/share/doc/python-django-filters-doc/html/index.html
|
||||
Files: /usr/share/doc/python-django-filters-doc/html/*.html
|
|
@ -0,0 +1 @@
|
|||
docs/.build/html
|
|
@ -0,0 +1 @@
|
|||
README.rst
|
|
@ -1,5 +1,25 @@
|
|||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
export PYBUILD_NAME=django-filters
|
||||
|
||||
%:
|
||||
dh $@ --with python2 --buildsystem=python_distutils
|
||||
dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
|
||||
|
||||
.PHONY: override_dh_auto_build
|
||||
override_dh_auto_build:
|
||||
PYTHONPATH=. sphinx-build -b html -d docs/.build/.doctrees -N docs docs/.build/html
|
||||
dh_auto_build
|
||||
set -e; \
|
||||
for loc in django_filters/locale/*; do \
|
||||
test -r $$loc/LC_MESSAGES/django.po &&\
|
||||
python3 setup.py compile_catalog --directory django_filters/locale/ --locale $$(basename $$loc) --domain django; \
|
||||
done
|
||||
|
||||
.PHONY: override_dh_auto_test
|
||||
override_dh_auto_test:
|
||||
dh_auto_test -- --system=custom --test-args="{interpreter} ./runtests.py"
|
||||
|
||||
override_dh_clean:
|
||||
rm -rf docs/.build
|
||||
dh_clean
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
extend-diff-ignore="\.egg-info$"
|
|
@ -0,0 +1,3 @@
|
|||
version=3
|
||||
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
|
||||
http://pypi.debian.net/django-filter/django-filter-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
django_filters
|
|
@ -1,17 +1,19 @@
|
|||
# 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 we simply set None.
|
||||
try:
|
||||
# If DRF is not installed, no-op.
|
||||
if pkgutil.find_loader('rest_framework') is not None:
|
||||
from . import rest_framework
|
||||
except ImportError:
|
||||
rest_framework = None
|
||||
del pkgutil
|
||||
|
||||
__version__ = '1.0.1'
|
||||
__version__ = '1.1.0'
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
|
|
|
@ -3,7 +3,12 @@ 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:
|
||||
|
@ -11,7 +16,9 @@ try:
|
|||
except ImportError:
|
||||
crispy_forms = None
|
||||
|
||||
is_crispy = 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
|
||||
|
||||
def is_crispy():
|
||||
return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
|
||||
|
||||
|
||||
# coreapi is optional (Note that uritemplate is a dependency of coreapi)
|
||||
|
@ -22,6 +29,10 @@ try:
|
|||
except ImportError:
|
||||
coreapi = None
|
||||
|
||||
try:
|
||||
import coreschema
|
||||
except ImportError:
|
||||
coreschema = None
|
||||
|
||||
def remote_field(field):
|
||||
"""
|
||||
|
@ -49,3 +60,12 @@ 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)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
|
||||
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 _
|
||||
|
@ -6,7 +8,6 @@ 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,
|
||||
|
@ -63,32 +64,30 @@ DEFAULTS = {
|
|||
|
||||
DEPRECATED_SETTINGS = [
|
||||
'HELP_TEXT_FILTER',
|
||||
'HELP_TEXT_EXCLUDE'
|
||||
'HELP_TEXT_EXCLUDE',
|
||||
]
|
||||
|
||||
|
||||
def is_callable(value):
|
||||
# check for callables, except types
|
||||
return callable(value) and not isinstance(value, type)
|
||||
|
||||
|
||||
class Settings(object):
|
||||
|
||||
def __init__(self):
|
||||
for setting in DEFAULTS:
|
||||
value = self.get_setting(setting)
|
||||
setattr(self, setting, value)
|
||||
def __getattr__(self, name):
|
||||
if name not in DEFAULTS:
|
||||
msg = "'%s' object has no attribute '%s'"
|
||||
raise AttributeError(msg % (self.__class__.__name__, name))
|
||||
|
||||
def VERBOSE_LOOKUPS():
|
||||
"""
|
||||
VERBOSE_LOOKUPS accepts a dictionary of {terms: verbose expressions}
|
||||
or a zero-argument callable that returns a dictionary.
|
||||
"""
|
||||
def fget(self):
|
||||
if callable(self._VERBOSE_LOOKUPS):
|
||||
self._VERBOSE_LOOKUPS = self._VERBOSE_LOOKUPS()
|
||||
return self._VERBOSE_LOOKUPS
|
||||
value = self.get_setting(name)
|
||||
|
||||
def fset(self, value):
|
||||
self._VERBOSE_LOOKUPS = value
|
||||
if is_callable(value):
|
||||
value = value()
|
||||
|
||||
return locals()
|
||||
VERBOSE_LOOKUPS = property(**VERBOSE_LOOKUPS())
|
||||
# Cache the result
|
||||
setattr(self, name, value)
|
||||
return value
|
||||
|
||||
def get_setting(self, setting):
|
||||
django_setting = 'FILTERS_%s' % setting
|
||||
|
@ -107,9 +106,11 @@ class Settings(object):
|
|||
if setting not in DEFAULTS:
|
||||
return
|
||||
|
||||
# if exiting, refetch the value from settings.
|
||||
value = value if enter else self.get_setting(setting)
|
||||
setattr(self, setting, value)
|
||||
# if exiting, delete value to repopulate
|
||||
if enter:
|
||||
setattr(self, setting, value)
|
||||
else:
|
||||
delattr(self, setting)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
ALL_FIELDS = '__all__'
|
||||
|
||||
|
||||
class STRICTNESS:
|
||||
class IGNORE:
|
||||
EMPTY_VALUES = ([], (), {}, '', None)
|
||||
|
||||
|
||||
class STRICTNESS(object):
|
||||
class IGNORE(object):
|
||||
pass
|
||||
|
||||
class RETURN_NO_RESULTS:
|
||||
class RETURN_NO_RESULTS(object):
|
||||
pass
|
||||
|
||||
class RAISE_VALIDATION_ERROR:
|
||||
class RAISE_VALIDATION_ERROR(object):
|
||||
pass
|
||||
|
||||
# Values of False & True chosen for backward compatability reasons.
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, time
|
||||
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 RangeWidget, LookupTypeWidget, CSVWidget, BaseCSVWidget
|
||||
from .widgets import BaseCSVWidget, CSVWidget, LookupTypeWidget, RangeWidget
|
||||
|
||||
|
||||
class RangeField(forms.MultiValueField):
|
||||
|
@ -43,10 +43,14 @@ class DateRangeField(RangeField):
|
|||
start_date, stop_date = data_list
|
||||
if start_date:
|
||||
start_date = handle_timezone(
|
||||
datetime.combine(start_date, time.min))
|
||||
datetime.combine(start_date, time.min),
|
||||
False
|
||||
)
|
||||
if stop_date:
|
||||
stop_date = handle_timezone(
|
||||
datetime.combine(stop_date, time.max))
|
||||
datetime.combine(stop_date, time.max),
|
||||
False
|
||||
)
|
||||
return slice(start_date, stop_date)
|
||||
return None
|
||||
|
||||
|
@ -88,6 +92,7 @@ class LookupTypeField(forms.MultiValueField):
|
|||
}
|
||||
widget = LookupTypeWidget(**defaults)
|
||||
kwargs['widget'] = widget
|
||||
kwargs['help_text'] = field.help_text
|
||||
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
|
@ -176,3 +181,114 @@ class BaseRangeField(BaseCSVField):
|
|||
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
|
||||
|
|
|
@ -1,25 +1,36 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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.sql.constants import QUERY_TERMS
|
||||
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 (
|
||||
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||
BaseCSVField,
|
||||
BaseRangeField,
|
||||
ChoiceField,
|
||||
DateRangeField,
|
||||
DateTimeRangeField,
|
||||
IsoDateTimeField,
|
||||
Lookup,
|
||||
LookupTypeField,
|
||||
ModelChoiceField,
|
||||
ModelMultipleChoiceField,
|
||||
MultipleChoiceField,
|
||||
RangeField,
|
||||
TimeRangeField
|
||||
)
|
||||
from .utils import label_for_filter, pretty_name
|
||||
|
||||
from .utils import deprecate, label_for_filter
|
||||
|
||||
__all__ = [
|
||||
'AllValuesFilter',
|
||||
|
@ -56,26 +67,40 @@ __all__ = [
|
|||
LOOKUP_TYPES = sorted(QUERY_TERMS)
|
||||
|
||||
|
||||
EMPTY_VALUES = ([], (), {}, '', None)
|
||||
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, name=None, label=None, widget=None, method=None, lookup_expr='exact',
|
||||
required=False, distinct=False, exclude=False, **kwargs):
|
||||
self.name = name
|
||||
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.widget = widget
|
||||
self.required = required
|
||||
self.extra = kwargs
|
||||
self.distinct = distinct
|
||||
self.exclude = exclude
|
||||
|
||||
self.extra = kwargs
|
||||
self.extra.setdefault('required', False)
|
||||
|
||||
self.creation_counter = Filter.creation_counter
|
||||
Filter.creation_counter += 1
|
||||
|
||||
|
@ -107,12 +132,24 @@ class Filter(object):
|
|||
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.name, self.lookup_expr, self.exclude
|
||||
model, self.field_name, self.lookup_expr, self.exclude
|
||||
)
|
||||
return self._label
|
||||
|
||||
|
@ -122,6 +159,10 @@ class Filter(object):
|
|||
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'):
|
||||
|
@ -151,13 +192,11 @@ class Filter(object):
|
|||
if x in self.lookup_expr:
|
||||
lookup.append(choice)
|
||||
|
||||
self._field = LookupTypeField(self.field_class(
|
||||
required=self.required, widget=self.widget, **field_kwargs),
|
||||
lookup, required=self.required, label=self.label)
|
||||
self._field = LookupTypeField(
|
||||
self.field_class(**field_kwargs), lookup,
|
||||
required=field_kwargs['required'], label=self.label)
|
||||
else:
|
||||
self._field = self.field_class(required=self.required,
|
||||
label=self.label, widget=self.widget,
|
||||
**field_kwargs)
|
||||
self._field = self.field_class(label=self.label, **field_kwargs)
|
||||
return self._field
|
||||
|
||||
def filter(self, qs, value):
|
||||
|
@ -170,7 +209,7 @@ class Filter(object):
|
|||
return qs
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, lookup): value})
|
||||
return qs
|
||||
|
||||
|
||||
|
@ -183,39 +222,17 @@ class BooleanFilter(Filter):
|
|||
|
||||
|
||||
class ChoiceFilter(Filter):
|
||||
field_class = forms.ChoiceField
|
||||
field_class = ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
|
||||
null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
|
||||
null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
|
||||
|
||||
self.null_value = null_value
|
||||
|
||||
if 'choices' in kwargs:
|
||||
choices = kwargs.get('choices')
|
||||
|
||||
# coerce choices to list
|
||||
if callable(choices):
|
||||
choices = choices()
|
||||
choices = list(choices)
|
||||
|
||||
# create the empty/null choices that prepend the original choices
|
||||
prepend = []
|
||||
if empty_label is not None:
|
||||
prepend.append(('', empty_label))
|
||||
if null_label is not None:
|
||||
prepend.append((null_value, null_label))
|
||||
|
||||
kwargs['choices'] = prepend + choices
|
||||
|
||||
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.name, self.lookup_expr): None})
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
|
@ -250,13 +267,14 @@ class MultipleChoiceFilter(Filter):
|
|||
`distinct` defaults to `True` as to-many relationships will generally
|
||||
require this.
|
||||
"""
|
||||
field_class = forms.MultipleChoiceField
|
||||
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):
|
||||
|
@ -268,7 +286,7 @@ class MultipleChoiceFilter(Filter):
|
|||
return False
|
||||
|
||||
# A reasonable default for being a noop...
|
||||
if self.required and len(value) == len(self.field.choices):
|
||||
if self.extra.get('required') and len(value) == len(self.field.choices):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -284,6 +302,8 @@ class MultipleChoiceFilter(Filter):
|
|||
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)
|
||||
|
@ -297,9 +317,9 @@ class MultipleChoiceFilter(Filter):
|
|||
|
||||
def get_filter_predicate(self, v):
|
||||
try:
|
||||
return {self.name: getattr(v, self.field.to_field_name)}
|
||||
return {self.field_name: getattr(v, self.field.to_field_name)}
|
||||
except (AttributeError, TypeError):
|
||||
return {self.name: v}
|
||||
return {self.field_name: v}
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
|
||||
|
@ -386,12 +406,16 @@ class QuerySetRequestMixin(object):
|
|||
return super(QuerySetRequestMixin, self).field
|
||||
|
||||
|
||||
class ModelChoiceFilter(QuerySetRequestMixin, Filter):
|
||||
field_class = forms.ModelChoiceField
|
||||
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 = forms.ModelMultipleChoiceField
|
||||
field_class = ModelMultipleChoiceField
|
||||
|
||||
|
||||
class NumberFilter(Filter):
|
||||
|
@ -404,13 +428,13 @@ class NumericRangeFilter(Filter):
|
|||
def filter(self, qs, value):
|
||||
if value:
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__%s' % (self.name, self.lookup_expr)
|
||||
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.name: value.start})
|
||||
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.name: value.stop})
|
||||
qs = self.get_method(qs)(**{'%s__endswith' % self.field_name: value.stop})
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
@ -422,13 +446,13 @@ class RangeFilter(Filter):
|
|||
def filter(self, qs, value):
|
||||
if value:
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__range' % self.name
|
||||
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.name: value.start})
|
||||
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.name: value.stop})
|
||||
qs = self.get_method(qs)(**{'%s__lte' % self.field_name: value.stop})
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
@ -480,7 +504,7 @@ class DateRangeFilter(ChoiceFilter):
|
|||
value = ''
|
||||
|
||||
assert value in self.options
|
||||
qs = self.options[value][1](qs, self.name)
|
||||
qs = self.options[value][1](qs, self.field_name)
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
@ -502,7 +526,7 @@ class AllValuesFilter(ChoiceFilter):
|
|||
@property
|
||||
def field(self):
|
||||
qs = self.model._default_manager.distinct()
|
||||
qs = qs.order_by(self.name).values_list(self.name, flat=True)
|
||||
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
|
||||
|
||||
|
@ -511,7 +535,7 @@ class AllValuesMultipleFilter(MultipleChoiceFilter):
|
|||
@property
|
||||
def field(self):
|
||||
qs = self.model._default_manager.distinct()
|
||||
qs = qs.order_by(self.name).values_list(self.name, flat=True)
|
||||
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
|
||||
|
||||
|
@ -619,6 +643,7 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter):
|
|||
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)
|
||||
|
||||
|
@ -685,7 +710,7 @@ class FilterMethod(object):
|
|||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
return self.method(qs, self.f.name, value)
|
||||
return self.method(qs, self.f.field_name, value)
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
|
@ -701,7 +726,7 @@ class FilterMethod(object):
|
|||
# otherwise, method is the name of a method on the parent FilterSet.
|
||||
assert hasattr(instance, 'parent'), \
|
||||
"Filter '%s' must have a parent FilterSet to find '.%s()'" % \
|
||||
(instance.name, instance.method)
|
||||
(instance.field_name, instance.method)
|
||||
|
||||
parent = instance.parent
|
||||
method = getattr(parent, instance.method, None)
|
||||
|
|
|
@ -1,63 +1,66 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.forms.forms import NON_FIELD_ERRORS
|
||||
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 .conf import settings
|
||||
from .compat import remote_field, remote_queryset
|
||||
from .constants import ALL_FIELDS, STRICTNESS
|
||||
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
|
||||
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter, DurationFilter)
|
||||
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
|
||||
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 get_filter_name(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])
|
||||
def _together_valid(form, fieldset):
|
||||
field_presence = [
|
||||
form.cleaned_data.get(field) not in EMPTY_VALUES
|
||||
for field in fieldset
|
||||
]
|
||||
|
||||
# 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
|
||||
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):
|
||||
|
||||
def add_error(message):
|
||||
try:
|
||||
form.add_error(None, message)
|
||||
except AttributeError:
|
||||
form._errors[NON_FIELD_ERRORS] = message
|
||||
|
||||
def all_valid(fieldset):
|
||||
cleaned_data = form.cleaned_data
|
||||
count = len([i for i in fieldset if cleaned_data.get(i)])
|
||||
return 0 < count < len(fieldset)
|
||||
|
||||
super(form.__class__, form).full_clean()
|
||||
message = 'Following fields must be together: %s'
|
||||
if isinstance(together[0], (list, tuple)):
|
||||
for each in together:
|
||||
if all_valid(each):
|
||||
return add_error(message % ','.join(each))
|
||||
elif all_valid(together):
|
||||
return add_error(message % ','.join(together))
|
||||
|
||||
for each in together:
|
||||
if not _together_valid(form, each):
|
||||
return form.add_error(None, message % ','.join(each))
|
||||
|
||||
return full_clean
|
||||
|
||||
|
||||
|
@ -73,6 +76,8 @@ class FilterSetOptions(object):
|
|||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -94,17 +99,21 @@ class FilterSetMetaclass(type):
|
|||
if isinstance(obj, Filter)
|
||||
]
|
||||
|
||||
# Default the `filter.name` to the attribute name on the filterset
|
||||
# Default the `filter.field_name` to the attribute name on the filterset
|
||||
for filter_name, f in filters:
|
||||
if getattr(f, 'name', None) is None:
|
||||
f.name = filter_name
|
||||
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 = list(base.declared_filters.items()) + filters
|
||||
filters = [
|
||||
(name, f) for name, f
|
||||
in base.declared_filters.items()
|
||||
if name not in attrs
|
||||
] + filters
|
||||
|
||||
return OrderedDict(filters)
|
||||
|
||||
|
@ -137,6 +146,7 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
|
|||
'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: {
|
||||
|
@ -144,6 +154,7 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
|
|||
'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: {
|
||||
|
@ -261,6 +272,22 @@ class BaseFilterSet(object):
|
|||
|
||||
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):
|
||||
"""
|
||||
|
@ -290,7 +317,7 @@ class BaseFilterSet(object):
|
|||
continue
|
||||
|
||||
for lookup_expr in lookups:
|
||||
filter_name = get_filter_name(field_name, lookup_expr)
|
||||
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:
|
||||
|
@ -314,11 +341,11 @@ class BaseFilterSet(object):
|
|||
return filters
|
||||
|
||||
@classmethod
|
||||
def filter_for_field(cls, f, name, lookup_expr='exact'):
|
||||
def filter_for_field(cls, f, field_name, lookup_expr='exact'):
|
||||
f, lookup_type = resolve_field(f, lookup_expr)
|
||||
|
||||
default = {
|
||||
'name': name,
|
||||
'field_name': field_name,
|
||||
'lookup_expr': lookup_expr,
|
||||
}
|
||||
|
||||
|
@ -329,16 +356,16 @@ class BaseFilterSet(object):
|
|||
"%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__, name, lookup_expr, f.__class__.__name__)
|
||||
) % (cls.__name__, field_name, lookup_expr, f.__class__.__name__)
|
||||
|
||||
return filter_class(**default)
|
||||
|
||||
@classmethod
|
||||
def filter_for_reverse_field(cls, f, name):
|
||||
def filter_for_reverse_field(cls, f, field_name):
|
||||
rel = remote_field(f.field)
|
||||
queryset = f.field.model._default_manager.all()
|
||||
default = {
|
||||
'name': name,
|
||||
'field_name': field_name,
|
||||
'queryset': queryset,
|
||||
}
|
||||
if rel.multiple:
|
||||
|
@ -421,8 +448,8 @@ class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
|
|||
pass
|
||||
|
||||
|
||||
def filterset_factory(model):
|
||||
meta = type(str('Meta'), (object,), {'model': model, 'fields': ALL_FIELDS})
|
||||
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.
|
@ -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"
|
|
@ -12,7 +12,7 @@ msgstr ""
|
|||
"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"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
|
|
@ -3,63 +3,199 @@
|
|||
# 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: 2015-07-25 01:24+0200\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"
|
||||
"Language: pl_PL\n"
|
||||
|
||||
#: filters.py:56
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "Jest to filtr wykluczający"
|
||||
#: conf.py:25
|
||||
#, fuzzy
|
||||
#| msgid "Any date"
|
||||
msgid "date"
|
||||
msgstr "Dowolna data"
|
||||
|
||||
#: filters.py:56
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
#: conf.py:26
|
||||
#, fuzzy
|
||||
#| msgid "This year"
|
||||
msgid "year"
|
||||
msgstr "Ten rok"
|
||||
|
||||
#: filters.py:226
|
||||
#: 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:227
|
||||
#: filters.py:453
|
||||
msgid "Today"
|
||||
msgstr "Dziś"
|
||||
|
||||
#: filters.py:232
|
||||
#: filters.py:458
|
||||
msgid "Past 7 days"
|
||||
msgstr "Ostatnie 7 dni"
|
||||
|
||||
#: filters.py:236
|
||||
#: filters.py:462
|
||||
msgid "This month"
|
||||
msgstr "Ten miesiąc"
|
||||
|
||||
#: filters.py:240
|
||||
#: filters.py:466
|
||||
msgid "This year"
|
||||
msgstr "Ten rok"
|
||||
|
||||
#: filters.py:243
|
||||
#: filters.py:469
|
||||
msgid "Yesterday"
|
||||
msgstr "Wczoraj"
|
||||
|
||||
#: filterset.py:423 filterset.py:432
|
||||
#: 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)"
|
||||
|
||||
#: filterset.py:434
|
||||
#: filters.py:630
|
||||
msgid "Ordering"
|
||||
msgstr "Sortowanie"
|
||||
|
||||
#: widgets.py:63
|
||||
#: 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"
|
||||
|
|
|
@ -12,7 +12,7 @@ msgstr ""
|
|||
"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"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
|
|
@ -1,46 +1,23 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.template import Template, TemplateDoesNotExist, loader
|
||||
import warnings
|
||||
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
from rest_framework.compat import template_render
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
|
||||
from . import filters, filterset
|
||||
from .. import compat
|
||||
from . import filterset
|
||||
|
||||
|
||||
CRISPY_TEMPLATE = """
|
||||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
||||
"""
|
||||
|
||||
|
||||
FILTER_TEMPLATE = """
|
||||
{% 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>
|
||||
"""
|
||||
|
||||
|
||||
if compat.is_crispy:
|
||||
template_path = 'django_filters/rest_framework/crispy_form.html'
|
||||
template_default = CRISPY_TEMPLATE
|
||||
|
||||
else:
|
||||
template_path = 'django_filters/rest_framework/form.html'
|
||||
template_default = FILTER_TEMPLATE
|
||||
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
class DjangoFilterBackend(object):
|
||||
default_filter_set = filterset.FilterSet
|
||||
template = template_path
|
||||
|
||||
@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):
|
||||
"""
|
||||
|
@ -59,8 +36,10 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
return filter_class
|
||||
|
||||
if filter_fields:
|
||||
MetaBase = getattr(self.default_filter_set, 'Meta', object)
|
||||
|
||||
class AutoFilterSet(self.default_filter_set):
|
||||
class Meta:
|
||||
class Meta(MetaBase):
|
||||
model = queryset.model
|
||||
fields = filter_fields
|
||||
|
||||
|
@ -82,24 +61,44 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
return None
|
||||
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
|
||||
|
||||
try:
|
||||
template = loader.get_template(self.template)
|
||||
except TemplateDoesNotExist:
|
||||
template = Template(template_default)
|
||||
|
||||
return template_render(template, context={
|
||||
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()`'
|
||||
filter_class = self.get_filter_class(view, view.get_queryset())
|
||||
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=False, location='query', description=six.text_type(field.field.help_text))
|
||||
for field_name, field in filter_class().filters.items()
|
||||
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()
|
||||
]
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
|
||||
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
|
||||
from .. import compat
|
||||
|
||||
if compat.is_crispy:
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit
|
||||
|
||||
|
||||
FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
FILTER_FOR_DBFIELD_DEFAULTS.update({
|
||||
|
@ -24,11 +22,15 @@ FILTER_FOR_DBFIELD_DEFAULTS.update({
|
|||
class FilterSet(filterset.FilterSet):
|
||||
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FilterSet, self).__init__(*args, **kwargs)
|
||||
@property
|
||||
def form(self):
|
||||
form = super(FilterSet, self).form
|
||||
|
||||
if compat.is_crispy:
|
||||
layout_components = list(self.form.fields.keys()) + [
|
||||
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()
|
||||
|
@ -36,4 +38,15 @@ class FilterSet(filterset.FilterSet):
|
|||
helper.template_pack = 'bootstrap3'
|
||||
helper.layout = Layout(*layout_components)
|
||||
|
||||
self.form.helper = helper
|
||||
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))
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% load crispy_forms_tags %}
|
||||
{% load i18n %}
|
||||
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
{% crispy filter.form %}
|
|
@ -0,0 +1,6 @@
|
|||
{% load i18n %}
|
||||
<h2>{% trans "Field filters" %}</h2>
|
||||
<form class="form" action="" method="get">
|
||||
{{ filter.form.as_p }}
|
||||
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
|
||||
</form>
|
|
@ -0,0 +1 @@
|
|||
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
|
|
@ -1,22 +1,20 @@
|
|||
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 RelatedField, ForeignObjectRel
|
||||
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 _
|
||||
|
||||
try:
|
||||
from django.forms.utils import pretty_name
|
||||
except ImportError: # Django 1.8
|
||||
from django.forms.forms import pretty_name
|
||||
|
||||
from .compat import remote_field, remote_model
|
||||
from .compat import make_aware, remote_field, remote_model
|
||||
from .exceptions import FieldLookupError
|
||||
|
||||
|
||||
|
@ -124,6 +122,10 @@ def resolve_field(model_field, lookup_expr):
|
|||
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.
|
||||
|
@ -133,20 +135,20 @@ def resolve_field(model_field, lookup_expr):
|
|||
# We didn't find a lookup. We are going to interpret
|
||||
# the name as transform, and do an Exact lookup against
|
||||
# it.
|
||||
lhs = query.try_transform(lhs, name, lookups)
|
||||
lhs = query.try_transform(*args)
|
||||
final_lookup = lhs.get_lookup('exact')
|
||||
return lhs.output_field, final_lookup.lookup_name
|
||||
lhs = query.try_transform(lhs, name, lookups)
|
||||
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):
|
||||
def handle_timezone(value, is_dst=None):
|
||||
if settings.USE_TZ and timezone.is_naive(value):
|
||||
return timezone.make_aware(value, timezone.get_default_timezone())
|
||||
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 timezone.make_naive(value, timezone.utc)
|
||||
return value
|
||||
|
||||
|
||||
|
@ -172,7 +174,10 @@ def verbose_field_name(model, field_name):
|
|||
names = []
|
||||
for part in parts:
|
||||
if isinstance(part, ForeignObjectRel):
|
||||
names.append(force_text(part.related_name))
|
||||
if part.related_name:
|
||||
names.append(part.related_name.replace('_', ' '))
|
||||
else:
|
||||
return '[invalid name]'
|
||||
else:
|
||||
names.append(force_text(part.verbose_name))
|
||||
|
||||
|
@ -224,6 +229,27 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False):
|
|||
verbose_expression += [verbose_lookup_expr(lookup_expr)]
|
||||
|
||||
verbose_expression = [force_text(part) for part in verbose_expression if part]
|
||||
verbose_expression = pretty_name(' '.join(verbose_expression))
|
||||
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
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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
|
||||
from django.views.generic.list import MultipleObjectTemplateResponseMixin
|
||||
from django.views.generic.list import (
|
||||
MultipleObjectMixin,
|
||||
MultipleObjectTemplateResponseMixin
|
||||
)
|
||||
|
||||
from .constants import ALL_FIELDS
|
||||
from .filterset import filterset_factory
|
||||
|
||||
|
||||
|
@ -12,6 +16,7 @@ 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):
|
||||
"""
|
||||
|
@ -20,7 +25,7 @@ class FilterMixin(object):
|
|||
if self.filterset_class:
|
||||
return self.filterset_class
|
||||
elif self.model:
|
||||
return filterset_factory(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__)
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import Iterable
|
||||
from itertools import chain
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except:
|
||||
from urllib import urlencode # noqa
|
||||
from re import search, sub
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms.widgets import flatatt
|
||||
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 _
|
||||
|
@ -36,7 +34,10 @@ class LinkWidget(forms.Widget):
|
|||
self.data = {}
|
||||
if value is None:
|
||||
value = ''
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
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:
|
||||
|
@ -80,19 +81,81 @@ class LinkWidget(forms.Widget):
|
|||
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]
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
return '-'.join(rendered_widgets)
|
||||
|
||||
|
||||
class LookupTypeWidget(forms.MultiWidget):
|
||||
def decompress(self, value):
|
||||
|
@ -158,7 +221,7 @@ class BaseCSVWidget(forms.Widget):
|
|||
|
||||
if len(value) <= 1:
|
||||
# delegate to main widget (Select, etc...) if not multiple values
|
||||
value = value[0] if value else value
|
||||
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
|
||||
|
@ -187,22 +250,19 @@ class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
|
|||
|
||||
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 []
|
||||
|
||||
if isinstance(values_list, string_types):
|
||||
values_list = [values_list]
|
||||
|
||||
# apparently its an array, so no need to process it's values as csv
|
||||
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
|
||||
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
|
||||
if len(values_list) > 1:
|
||||
if len(values_list) > 0:
|
||||
ret = [x for x in values_list if x]
|
||||
elif len(values_list) == 1:
|
||||
# treat first element as csv string
|
||||
# ?foo=1,2 -> data.getlist(foo) -> foo = ['1,2']
|
||||
ret = [x.strip() for x in values_list[0].rstrip(',').split(',') if x]
|
||||
else:
|
||||
ret = []
|
||||
|
||||
|
|
Binary file not shown.
|
@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
|
|||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.0.1'
|
||||
version = '1.1.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.0.1'
|
||||
release = '1.1.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
|
|
@ -65,3 +65,26 @@ supported versions of Python and Django. Install tox, and then simply run:
|
|||
|
||||
$ 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
|
||||
|
|
|
@ -10,24 +10,25 @@ Django-filter can be installed from PyPI with tools like ``pip``:
|
|||
|
||||
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||
|
||||
.. note::
|
||||
.. code-block:: python
|
||||
|
||||
django-filter provides *some* localization for *some* languages. If you do
|
||||
not need these translations (or would rather provide your own), then it is
|
||||
unnecessary to add django-filter to the ``INSTALLED_APPS`` setting.
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Django-filter is tested against all supported versions of Python and `Django`__,
|
||||
as well as the latest versions of Django REST Framework (`DRF`__).
|
||||
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.3, 3.4, 3.5
|
||||
* **Django**: 1.8, 1.9, 1.10
|
||||
* **DRF**: 3.4, 3.5
|
||||
* **Python**: 2.7, 3.4, 3.5, 3.6
|
||||
* **Django**: 1.10, 1.11
|
||||
* **DRF**: 3.7
|
||||
|
|
|
@ -1,6 +1,40 @@
|
|||
================
|
||||
===============
|
||||
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
|
||||
|
@ -8,6 +42,29 @@ 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
|
||||
--------------------------------------------------------
|
||||
|
@ -164,3 +221,11 @@ 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.
|
||||
|
|
|
@ -37,6 +37,12 @@ If you want to use the django-filter backend by default, add it to the ``DEFAULT
|
|||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
|
@ -58,8 +64,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet
|
|||
|
||||
|
||||
class ProductFilter(filters.FilterSet):
|
||||
min_price = django_filters.NumberFilter(name="price", lookup_expr='gte')
|
||||
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
|
||||
min_price = filters.NumberFilter(name="price", lookup_expr='gte')
|
||||
max_price = filters.NumberFilter(name="price", lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
|
@ -170,3 +176,14 @@ If you are using DRF's browsable API or admin API you may also want to install `
|
|||
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``.
|
||||
|
|
|
@ -143,7 +143,7 @@ the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
|
|||
)
|
||||
|
||||
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:
|
||||
|
@ -156,8 +156,6 @@ Filtering by an empty string
|
|||
It's not currently possible to filter by an empty string, since empty values are
|
||||
interpreted as a skipped filter.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield=
|
||||
|
||||
Solution 1: Magic values
|
||||
|
@ -166,8 +164,6 @@ Solution 1: Magic values
|
|||
You can override the ``filter()`` method of a filter class to specifically check
|
||||
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield=EMPTY
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -189,8 +185,6 @@ Solution 2: Empty string filter
|
|||
It would also be possible to create an empty value filter that exhibits the same
|
||||
behavior as an ``isnull`` filter.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET http://localhost/api/my-model?myfield__isempty=false
|
||||
|
||||
.. code-block:: python
|
||||
|
@ -213,3 +207,40 @@ behavior as an ``isnull`` filter.
|
|||
|
||||
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)
|
||||
|
|
|
@ -6,6 +6,11 @@ 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
|
||||
---------
|
||||
|
||||
|
@ -24,7 +29,7 @@ The filter
|
|||
----------
|
||||
|
||||
We have a number of fields and we want to let our users filter based on the
|
||||
price or the release_date. We create a ``FilterSet`` for this::
|
||||
name, the price or the release_date. We create a ``FilterSet`` for this::
|
||||
|
||||
import django_filters
|
||||
|
||||
|
@ -169,6 +174,11 @@ 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``
|
||||
"""""""""""""""""""""""""""""
|
||||
|
@ -189,8 +199,10 @@ those that are published and those that are owned by the logged-in user
|
|||
@property
|
||||
def qs(self):
|
||||
parent = super(ArticleFilter, self).qs
|
||||
author = getattr(self.request, 'user', None)
|
||||
|
||||
return parent.filter(is_published=True) \
|
||||
| parent.filter(author=request.user)
|
||||
| parent.filter(author=author)
|
||||
|
||||
|
||||
Filtering the related queryset for ``ModelChoiceFilter``
|
||||
|
@ -204,6 +216,9 @@ 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()
|
||||
|
||||
|
@ -297,6 +312,9 @@ You must provide either a ``model`` or ``filterset_class`` argument, similar to
|
|||
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.
|
||||
|
|
|
@ -356,9 +356,16 @@ 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()
|
||||
|
||||
|
@ -505,6 +512,14 @@ Example of using the ``DateField`` field::
|
|||
# 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):
|
||||
|
@ -621,7 +636,7 @@ filter is then used to validate the individual values.
|
|||
|
||||
Example::
|
||||
|
||||
class NumberInFilter(BaseInFilter, NumericFilter):
|
||||
class NumberInFilter(BaseInFilter, NumberFilter):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
|
@ -648,7 +663,7 @@ comma-separated values.
|
|||
|
||||
Example::
|
||||
|
||||
class NumberRangeFilter(BaseInFilter, NumericFilter):
|
||||
class NumberRangeFilter(BaseInFilter, NumberFilter):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
|
@ -742,7 +757,8 @@ 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.
|
||||
for APIs. ``SelectMultiple`` widgets are not compatible, given that they
|
||||
are not able to retain selection order.
|
||||
|
||||
Adding Custom filter choices
|
||||
""""""""""""""""""""""""""""
|
||||
|
@ -757,14 +773,16 @@ If you wish to sort by non-model fields, you'll need to add custom handling to a
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomOrderingFilter, self).__init__(*args, **kwargs)
|
||||
self.choices += [
|
||||
self.extra['choices'] += [
|
||||
('relevance', 'Relevance'),
|
||||
('-relevance', 'Relevance (descending)'),
|
||||
]
|
||||
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value in ['relevance', '-relevance']:
|
||||
# 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)
|
||||
|
|
|
@ -152,17 +152,33 @@ This is a map of model fields to filter classes with options::
|
|||
|
||||
.. _strict:
|
||||
|
||||
``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.
|
||||
|
||||
The ``strict`` option controls whether results are returned when an invalid
|
||||
value is specified by the user for any filter field. By default, ``strict`` is
|
||||
set to ``STRICTNESS.RETURN_NO_RESULTS`` meaning that an empty queryset is
|
||||
returned if any field contains an invalid value. You can loosen this behavior
|
||||
by setting ``strict`` to ``STRICTNESS.IGNORE`` which will effectively ignore a
|
||||
filter field if its value is invalid. A third option of
|
||||
``STRICTNESS.RAISE_VALIDATION_ERROR`` will cause a ``ValidationError`` to be
|
||||
raised if any field contains an invalid value.
|
||||
|
||||
Overriding ``FilterSet`` methods
|
||||
--------------------------------
|
||||
|
|
|
@ -94,4 +94,13 @@ FILTERS_STRICTNESS
|
|||
|
||||
Default: ``STRICTNESS.RETURN_NO_RESULTS``
|
||||
|
||||
Set the global default for FilterSet :ref:`strictness <strict>`.
|
||||
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
|
||||
|
|
|
@ -61,3 +61,28 @@ 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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
markdown==2.6.4
|
||||
coreapi
|
||||
django-crispy-forms
|
||||
|
||||
coverage
|
||||
mock
|
||||
|
|
16
setup.cfg
16
setup.cfg
|
@ -2,4 +2,18 @@
|
|||
license-file = LICENSE
|
||||
|
||||
[wheel]
|
||||
universal=1
|
||||
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
|
||||
|
||||
|
|
19
setup.py
19
setup.py
|
@ -6,7 +6,7 @@ f = open('README.rst')
|
|||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
version = '1.0.1'
|
||||
version = '1.1.0'
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
if os.system("pip freeze | grep wheel"):
|
||||
|
@ -32,13 +32,9 @@ setup(
|
|||
author_email='alex.gaynor@gmail.com',
|
||||
maintainer='Carlton Gibson',
|
||||
maintainer_email='carlton.gibson@noumenal.es',
|
||||
url='http://github.com/carltongibson/django-filter/tree/master',
|
||||
packages=find_packages(exclude=['tests']),
|
||||
package_data={
|
||||
'django_filters': [
|
||||
'locale/*/LC_MESSAGES/*',
|
||||
],
|
||||
},
|
||||
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',
|
||||
|
@ -46,14 +42,19 @@ setup(
|
|||
'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',
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
)
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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
|
||||
|
@ -131,6 +129,8 @@ 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):
|
||||
|
|
|
@ -1,28 +1,24 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
from unittest import skipIf
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.db.models import BooleanField
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.dateparse import parse_date
|
||||
|
||||
try:
|
||||
from django.urls import reverse
|
||||
except ImportError:
|
||||
# Django < 1.10 compatibility
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from rest_framework import generics, serializers, status
|
||||
from rest_framework import generics, serializers
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from django_filters import compat, filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
|
||||
from django_filters.rest_framework import backends
|
||||
from django_filters.rest_framework import (
|
||||
DjangoFilterBackend,
|
||||
FilterSet,
|
||||
backends
|
||||
)
|
||||
|
||||
from .models import BaseFilterableItem, BasicModel, FilterableItem, DjangoFilterOrderingModel
|
||||
from .models import FilterableItem
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
@ -59,69 +55,6 @@ class FilterClassRootView(generics.ListCreateAPIView):
|
|||
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'),
|
||||
]
|
||||
|
||||
|
||||
@skipIf(compat.coreapi is None, 'coreapi must be installed')
|
||||
class GetSchemaFieldsTests(TestCase):
|
||||
def test_fields_with_filter_fields_list(self):
|
||||
|
@ -131,6 +64,27 @@ class GetSchemaFieldsTests(TestCase):
|
|||
|
||||
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 = {
|
||||
|
@ -146,181 +100,57 @@ class GetSchemaFieldsTests(TestCase):
|
|||
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 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)
|
||||
|
||||
class TemplateTests(TestCase):
|
||||
def test_backend_output(self):
|
||||
"""
|
||||
Ensure backend renders default if template path does not exist
|
||||
|
@ -367,99 +197,33 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
|
|||
|
||||
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'}
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.rest_framework.test_backends')
|
||||
class IntegrationTestDetailFiltering(CommonFilteringTestCase):
|
||||
"""
|
||||
Integration tests for filtered detail views.
|
||||
"""
|
||||
def _get_url(self, item):
|
||||
return reverse('detail-view', kwargs=dict(pk=item.pk))
|
||||
|
||||
def test_get_filtered_detail_view(self):
|
||||
"""
|
||||
GET requests to filtered RetrieveAPIView that have a filter_class set
|
||||
should return filtered results.
|
||||
"""
|
||||
item = self.objects.all()[0]
|
||||
data = self._serialize_object(item)
|
||||
|
||||
# Basic test with no filter.
|
||||
response = self.client.get(self._get_url(item))
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, data)
|
||||
|
||||
# Tests that the decimal filter set that should fail.
|
||||
search_decimal = Decimal('4.25')
|
||||
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(high_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Tests that the decimal filter set that should succeed.
|
||||
search_decimal = Decimal('4.25')
|
||||
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
|
||||
low_item_data = self._serialize_object(low_item)
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(low_item)),
|
||||
{'decimal': '{param}'.format(param=search_decimal)})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, low_item_data)
|
||||
|
||||
# Tests that multiple filters works.
|
||||
search_decimal = Decimal('5.25')
|
||||
search_date = datetime.date(2012, 10, 2)
|
||||
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
|
||||
valid_item_data = self._serialize_object(valid_item)
|
||||
response = self.client.get(
|
||||
'{url}'.format(url=self._get_url(valid_item)), {
|
||||
'decimal': '{decimal}'.format(decimal=search_decimal),
|
||||
'date': '{date}'.format(date=search_date)
|
||||
})
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, valid_item_data)
|
||||
# multiple DTL backends
|
||||
with override_settings(TEMPLATES=[DTL, ALT]):
|
||||
self.test_backend_output()
|
||||
|
||||
|
||||
class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DjangoFilterOrderingModel
|
||||
fields = '__all__'
|
||||
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 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'
|
||||
}]
|
||||
class Backend(DjangoFilterBackend):
|
||||
default_filter_set = F
|
||||
|
||||
for d in data:
|
||||
DjangoFilterOrderingModel.objects.create(**d)
|
||||
view = FilterFieldsRootView()
|
||||
backend = Backend()
|
||||
|
||||
def test_default_ordering(self):
|
||||
class DjangoFilterOrderingView(generics.ListAPIView):
|
||||
serializer_class = DjangoFilterOrderingSerializer
|
||||
queryset = DjangoFilterOrderingModel.objects.all()
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
filter_fields = ['text']
|
||||
ordering = ('-date',)
|
||||
filter_class = backend.get_filter_class(view, view.get_queryset())
|
||||
filter_overrides = filter_class._meta.filter_overrides
|
||||
|
||||
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'}
|
||||
]
|
||||
)
|
||||
# derived filter_class.Meta should inherit from default_filter_set.Meta
|
||||
self.assertIn(BooleanField, filter_overrides)
|
||||
self.assertDictEqual(filter_overrides[BooleanField], {})
|
||||
|
|
|
@ -12,4 +12,4 @@ class BooleanFilterTests(TestCase):
|
|||
# from `rest_framework.filters`.
|
||||
f = filters.BooleanFilter()
|
||||
|
||||
self.assertEqual(f.widget, BooleanWidget)
|
||||
self.assertEqual(f.extra['widget'], BooleanWidget)
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
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 User, Article
|
||||
from ..models import Article, User
|
||||
|
||||
|
||||
class ArticleFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['author']
|
||||
|
||||
|
||||
class FilterSetFilterForFieldTests(TestCase):
|
||||
|
@ -19,4 +29,17 @@ class FilterSetFilterForFieldTests(TestCase):
|
|||
field = User._meta.get_field('is_active')
|
||||
result = FilterSet.filter_for_field(field, 'is_active')
|
||||
self.assertIsInstance(result, filters.BooleanFilter)
|
||||
self.assertEqual(result.widget, BooleanWidget)
|
||||
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'))
|
||||
|
|
|
@ -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'}
|
||||
]
|
||||
)
|
|
@ -1,3 +1,8 @@
|
|||
|
||||
# ensure package/conf is importable
|
||||
from django_filters import STRICTNESS
|
||||
from django_filters.conf import DEFAULTS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
|
@ -34,5 +39,7 @@ STATIC_URL = '/static/'
|
|||
|
||||
# help verify that DEFAULTS is importable from conf.
|
||||
def FILTERS_VERBOSE_LOOKUPS():
|
||||
from django_filters.conf import DEFAULTS
|
||||
return DEFAULTS['VERBOSE_LOOKUPS']
|
||||
|
||||
|
||||
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,9 +1,8 @@
|
|||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.conf import settings
|
||||
from django_filters import FilterSet, STRICTNESS
|
||||
|
||||
from django_filters import STRICTNESS, FilterSet
|
||||
from django_filters.conf import is_callable, settings
|
||||
from tests.models import User
|
||||
|
||||
|
||||
|
@ -119,3 +118,24 @@ class OverrideSettingsTests(TestCase):
|
|||
|
||||
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))
|
||||
|
|
|
@ -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))
|
|
@ -1,18 +1,25 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, time, timedelta, tzinfo
|
||||
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.timezone import make_aware, get_default_timezone
|
||||
from django.utils import timezone
|
||||
|
||||
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
|
||||
from django_filters.fields import (
|
||||
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
|
||||
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
|
||||
BaseCSVField,
|
||||
BaseRangeField,
|
||||
DateRangeField,
|
||||
DateTimeRangeField,
|
||||
IsoDateTimeField,
|
||||
Lookup,
|
||||
LookupTypeField,
|
||||
RangeField,
|
||||
TimeRangeField
|
||||
)
|
||||
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
|
||||
|
||||
|
||||
def to_d(float_value):
|
||||
|
@ -41,11 +48,12 @@ class RangeFieldTests(TestCase):
|
|||
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = RangeField(widget=w)
|
||||
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):
|
||||
|
@ -57,11 +65,12 @@ class DateRangeFieldTests(TestCase):
|
|||
@override_settings(USE_TZ=False)
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = DateRangeField(widget=w)
|
||||
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):
|
||||
|
@ -104,10 +113,13 @@ class LookupTypeFieldTests(TestCase):
|
|||
|
||||
def test_clean(self):
|
||||
inner = forms.DecimalField()
|
||||
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
|
||||
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()
|
||||
|
@ -129,48 +141,68 @@ class LookupTypeFieldTests(TestCase):
|
|||
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):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
d = self.parse_input(self.reference_str)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_string_with_timezone_is_parsed(self):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
d = self.parse_input(self.reference_str + "+01:00")
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_zulu(self):
|
||||
f = IsoDateTimeField()
|
||||
d = f.strptime(self.reference_str + "Z", IsoDateTimeField.ISO_8601)
|
||||
d = self.parse_input(self.reference_str + "Z")
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
@override_settings(TIME_ZONE='UTC')
|
||||
def test_datetime_timezone_awareness(self):
|
||||
# parsed datetimes should obey USE_TZ
|
||||
f = IsoDateTimeField()
|
||||
r = make_aware(self.reference_dt, get_default_timezone())
|
||||
utc, tokyo = pytz.timezone('UTC'), pytz.timezone('Asia/Tokyo')
|
||||
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
||||
self.assertEqual(d, r + r.utcoffset() - d.utcoffset())
|
||||
# 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)
|
||||
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(isinstance(d.tzinfo, tzinfo))
|
||||
self.assertEqual(d, r)
|
||||
# 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):
|
||||
# parsed datetimes should obey USE_TZ
|
||||
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()
|
||||
r = self.reference_dt.replace()
|
||||
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)
|
||||
|
||||
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(d.tzinfo is None)
|
||||
self.assertEqual(d, r - timedelta(hours=1))
|
||||
|
||||
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
|
||||
self.assertTrue(d.tzinfo is None)
|
||||
self.assertEqual(d, r)
|
||||
def test_datetime_wrong_format(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.parse_input('19-07-2015T51:34:13.759')
|
||||
|
||||
|
||||
class BaseCSVFieldTests(TestCase):
|
||||
|
@ -202,9 +234,10 @@ class BaseCSVFieldTests(TestCase):
|
|||
self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
|
||||
self.assertIn("RangeWidget", msg)
|
||||
|
||||
widget = CSVWidget()
|
||||
widget = CSVWidget(attrs={'class': 'class'})
|
||||
field = BaseCSVField(widget=widget)
|
||||
self.assertIs(field.widget, widget)
|
||||
self.assertIsInstance(field.widget, CSVWidget)
|
||||
self.assertEqual(field.widget.attrs, {'class': 'class'})
|
||||
|
||||
field = BaseCSVField(widget=CSVWidget)
|
||||
self.assertIsInstance(field.widget, CSVWidget)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
import mock
|
||||
|
@ -8,42 +7,45 @@ import unittest
|
|||
import django
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import six
|
||||
from django.utils import six, timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils import timezone
|
||||
|
||||
from django_filters.filterset import FilterSet
|
||||
from django_filters.filters import AllValuesFilter
|
||||
from django_filters.filters import AllValuesMultipleFilter
|
||||
from django_filters.filters import CharFilter
|
||||
from django_filters.filters import ChoiceFilter
|
||||
from django_filters.filters import DateRangeFilter
|
||||
from django_filters.filters import DateFromToRangeFilter
|
||||
from django_filters.filters import DateTimeFromToRangeFilter
|
||||
from django_filters.filters import DurationFilter
|
||||
from django_filters.filters import MultipleChoiceFilter
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filters import TypedMultipleChoiceFilter
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import NumberFilter
|
||||
from django_filters.filters import OrderingFilter
|
||||
from django_filters.filters import RangeFilter
|
||||
from django_filters.filters import TimeRangeFilter
|
||||
from django_filters.exceptions import FieldLookupError
|
||||
from django_filters.filters import (
|
||||
AllValuesFilter,
|
||||
AllValuesMultipleFilter,
|
||||
CharFilter,
|
||||
ChoiceFilter,
|
||||
DateFromToRangeFilter,
|
||||
DateRangeFilter,
|
||||
DateTimeFromToRangeFilter,
|
||||
DurationFilter,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
NumberFilter,
|
||||
OrderingFilter,
|
||||
RangeFilter,
|
||||
TimeRangeFilter,
|
||||
TypedMultipleChoiceFilter
|
||||
)
|
||||
from django_filters.filterset import FilterSet
|
||||
|
||||
from .models import User
|
||||
from .models import Comment
|
||||
from .models import Book
|
||||
from .models import Article
|
||||
from .models import Company
|
||||
from .models import Location
|
||||
from .models import Account
|
||||
from .models import BankAccount
|
||||
from .models import Profile
|
||||
from .models import Node
|
||||
from .models import DirectedNode
|
||||
from .models import STATUS_CHOICES
|
||||
from .models import SpacewalkRecord
|
||||
from .models import (
|
||||
STATUS_CHOICES,
|
||||
Account,
|
||||
Article,
|
||||
BankAccount,
|
||||
Book,
|
||||
Comment,
|
||||
Company,
|
||||
DirectedNode,
|
||||
Location,
|
||||
Node,
|
||||
Profile,
|
||||
SpacewalkRecord,
|
||||
User
|
||||
)
|
||||
|
||||
|
||||
class CharFilterTests(TestCase):
|
||||
|
@ -252,6 +254,44 @@ class MultipleChoiceFilterTests(TestCase):
|
|||
self.assertQuerysetEqual(
|
||||
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
|
||||
|
||||
def test_filtering_on_null_choice(self):
|
||||
User.objects.create(username='alex', status=1)
|
||||
User.objects.create(username='jacob', status=2)
|
||||
User.objects.create(username='aaron', status=2)
|
||||
User.objects.create(username='carl', status=0)
|
||||
|
||||
Article.objects.create(author_id=1, published=now())
|
||||
Article.objects.create(author_id=2, published=now())
|
||||
Article.objects.create(author_id=3, published=now())
|
||||
Article.objects.create(author_id=4, published=now())
|
||||
Article.objects.create(author_id=None, published=now())
|
||||
|
||||
choices = [(u.pk, str(u)) for u in User.objects.order_by('id')]
|
||||
|
||||
class F(FilterSet):
|
||||
author = MultipleChoiceFilter(
|
||||
choices=choices,
|
||||
null_value='null',
|
||||
null_label='NULL',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['author']
|
||||
|
||||
# sanity check to make sure the filter is setup correctly
|
||||
f = F({'author': ['1']})
|
||||
self.assertQuerysetEqual(f.qs, ['alex'], lambda o: str(o.author), False)
|
||||
|
||||
f = F({'author': ['null']})
|
||||
self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False)
|
||||
|
||||
f = F({'author': ['1', 'null']})
|
||||
self.assertQuerysetEqual(
|
||||
f.qs, ['alex', None],
|
||||
lambda o: o.author and str(o.author),
|
||||
False)
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -485,6 +525,21 @@ class ModelChoiceFilterTests(TestCase):
|
|||
f = F({'author': jacob.pk}, queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False)
|
||||
|
||||
@override_settings(FILTERS_NULL_CHOICE_LABEL='No Author')
|
||||
def test_filtering_null(self):
|
||||
Article.objects.create(published=now())
|
||||
alex = User.objects.create(username='alex')
|
||||
Article.objects.create(author=alex, published=now())
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['author', 'name']
|
||||
|
||||
qs = Article.objects.all()
|
||||
f = F({'author': 'null'}, queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False)
|
||||
|
||||
def test_callable_queryset(self):
|
||||
# Sanity check for callable queryset arguments.
|
||||
# Ensure that nothing is improperly cached
|
||||
|
@ -528,8 +583,8 @@ class ModelMultipleChoiceFilterTests(TestCase):
|
|||
average_rating=3.0)
|
||||
Book.objects.create(title="Stranger in a Strage Land", price='1.00',
|
||||
average_rating=3.0)
|
||||
alex.favorite_books = [b1, b2]
|
||||
aaron.favorite_books = [b1, b3]
|
||||
alex.favorite_books.add(b1, b2)
|
||||
aaron.favorite_books.add(b1, b3)
|
||||
|
||||
self.alex = alex
|
||||
|
||||
|
@ -552,6 +607,18 @@ class ModelMultipleChoiceFilterTests(TestCase):
|
|||
f = F({'favorite_books': ['4']}, queryset=qs)
|
||||
self.assertQuerysetEqual(f.qs, [], lambda o: o.username)
|
||||
|
||||
@override_settings(FILTERS_NULL_CHOICE_LABEL='No Favorites')
|
||||
def test_filtering_null(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['favorite_books']
|
||||
|
||||
qs = User.objects.all()
|
||||
f = F({'favorite_books': ['null']}, queryset=qs)
|
||||
|
||||
self.assertQuerysetEqual(f.qs, ['jacob'], lambda o: o.username)
|
||||
|
||||
def test_filtering_dictionary(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
|
@ -584,7 +651,7 @@ class ModelMultipleChoiceFilterTests(TestCase):
|
|||
'queryset': Book.objects.filter(id__in=[1, 2])
|
||||
})
|
||||
|
||||
self.filters['favorite_books'].required = True
|
||||
self.filters['favorite_books'].extra['required'] = True
|
||||
|
||||
qs = User.objects.all().order_by('username')
|
||||
|
||||
|
@ -874,6 +941,92 @@ class DateFromToRangeFilterTests(TestCase):
|
|||
'published_1': '2016-01-03'})
|
||||
self.assertEqual(len(results.qs), 3)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='America/Sao_Paulo')
|
||||
def test_filtering_dst_start_midnight(self):
|
||||
tz = timezone.get_default_timezone()
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 14, 23, 59)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 0, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 1, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 16, 0, 0)))
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
results = F(data={
|
||||
'published_0': '2017-10-15',
|
||||
'published_1': '2017-10-15'})
|
||||
self.assertEqual(len(results.qs), 2)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='America/Sao_Paulo')
|
||||
def test_filtering_dst_ends_midnight(self):
|
||||
tz = timezone.get_default_timezone()
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 19, 0, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 23, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 0, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 17, 15, 0)))
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
results = F(data={
|
||||
'published_0': '2017-02-18',
|
||||
'published_1': '2017-02-18'})
|
||||
self.assertEqual(len(results.qs), 2)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='Europe/Paris')
|
||||
def test_filtering_dst_start(self):
|
||||
tz = timezone.get_default_timezone()
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 25, 23, 59)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 0, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 2, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 3, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 27, 0, 0)))
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
results = F(data={
|
||||
'published_0': '2017-3-26',
|
||||
'published_1': '2017-3-26'})
|
||||
self.assertEqual(len(results.qs), 3)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='Europe/Paris')
|
||||
def test_filtering_dst_end(self):
|
||||
tz = timezone.get_default_timezone()
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 28, 23, 59)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 0, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 2, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 3, 0)))
|
||||
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 30, 0, 0)))
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
results = F(data={
|
||||
'published_0': '2017-10-29',
|
||||
'published_1': '2017-10-29'})
|
||||
self.assertEqual(len(results.qs), 3)
|
||||
|
||||
|
||||
class DateTimeFromToRangeFilterTests(TestCase):
|
||||
|
||||
|
@ -1290,8 +1443,8 @@ class M2MRelationshipTests(TestCase):
|
|||
average_rating=4.0)
|
||||
Book.objects.create(title="Stranger in a Strage Land", price='2.00',
|
||||
average_rating=3.0)
|
||||
alex.favorite_books = [b1, b2]
|
||||
aaron.favorite_books = [b1, b3]
|
||||
alex.favorite_books.add(b1, b2)
|
||||
aaron.favorite_books.add(b1, b3)
|
||||
|
||||
def test_m2m_relation(self):
|
||||
class F(FilterSet):
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import date, time, timedelta, datetime
|
||||
import inspect
|
||||
import mock
|
||||
from collections import OrderedDict
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
|
@ -14,41 +13,42 @@ from django.utils.translation import ugettext as _
|
|||
|
||||
from django_filters import filters, widgets
|
||||
from django_filters.fields import (
|
||||
Lookup,
|
||||
RangeField,
|
||||
BaseCSVField,
|
||||
DateRangeField,
|
||||
DateTimeRangeField,
|
||||
TimeRangeField,
|
||||
Lookup,
|
||||
LookupTypeField,
|
||||
BaseCSVField)
|
||||
RangeField,
|
||||
TimeRangeField
|
||||
)
|
||||
from django_filters.filters import (
|
||||
Filter,
|
||||
CharFilter,
|
||||
BooleanFilter,
|
||||
ChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
TypedMultipleChoiceFilter,
|
||||
DateFilter,
|
||||
DateTimeFilter,
|
||||
TimeFilter,
|
||||
DurationFilter,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
NumberFilter,
|
||||
NumericRangeFilter,
|
||||
RangeFilter,
|
||||
DateRangeFilter,
|
||||
DateFromToRangeFilter,
|
||||
DateTimeFromToRangeFilter,
|
||||
TimeRangeFilter,
|
||||
LOOKUP_TYPES,
|
||||
AllValuesFilter,
|
||||
BaseCSVFilter,
|
||||
BaseInFilter,
|
||||
BaseRangeFilter,
|
||||
UUIDFilter,
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
ChoiceFilter,
|
||||
DateFilter,
|
||||
DateFromToRangeFilter,
|
||||
DateRangeFilter,
|
||||
DateTimeFilter,
|
||||
DateTimeFromToRangeFilter,
|
||||
DurationFilter,
|
||||
Filter,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
NumberFilter,
|
||||
NumericRangeFilter,
|
||||
OrderingFilter,
|
||||
LOOKUP_TYPES)
|
||||
|
||||
RangeFilter,
|
||||
TimeFilter,
|
||||
TimeRangeFilter,
|
||||
TypedMultipleChoiceFilter,
|
||||
UUIDFilter
|
||||
)
|
||||
from tests.models import Book, User
|
||||
|
||||
|
||||
|
@ -132,17 +132,16 @@ class FilterTests(TestCase):
|
|||
f.field
|
||||
mocked.assert_called_once_with(required=mock.ANY,
|
||||
label=mock.ANY,
|
||||
widget=mock.ANY,
|
||||
someattr='someattr')
|
||||
|
||||
def test_field_with_required_filter(self):
|
||||
def test_field_required_default(self):
|
||||
# filter form fields should not be required by default
|
||||
with mock.patch.object(Filter, 'field_class',
|
||||
spec=['__call__']) as mocked:
|
||||
f = Filter(required=True)
|
||||
f = Filter()
|
||||
f.field
|
||||
mocked.assert_called_once_with(required=True,
|
||||
label=mock.ANY,
|
||||
widget=mock.ANY)
|
||||
mocked.assert_called_once_with(required=False,
|
||||
label=mock.ANY)
|
||||
|
||||
def test_filtering(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
|
@ -313,35 +312,35 @@ class ChoiceFilterTests(TestCase):
|
|||
def test_empty_choice(self):
|
||||
# default value
|
||||
f = ChoiceFilter(choices=[('a', 'a')])
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set value, allow blank label
|
||||
f = ChoiceFilter(choices=[('a', 'a')], empty_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# disable empty choice w/ None
|
||||
f = ChoiceFilter(choices=[('a', 'a')], empty_label=None)
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
def test_null_choice(self):
|
||||
# default is to be disabled
|
||||
f = ChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set label, allow blank label
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('null', ''),
|
||||
('a', 'a'),
|
||||
|
@ -349,7 +348,7 @@ class ChoiceFilterTests(TestCase):
|
|||
|
||||
# set null value
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='')
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('NULL', ''),
|
||||
('a', 'a'),
|
||||
|
@ -357,35 +356,73 @@ class ChoiceFilterTests(TestCase):
|
|||
|
||||
# explicitly disable
|
||||
f = ChoiceFilter(choices=[('a', 'a')], null_label=None)
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
def test_null_multiplechoice(self):
|
||||
# default is to be disabled
|
||||
f = MultipleChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set label, allow blank label
|
||||
f = MultipleChoiceFilter(choices=[('a', 'a')], null_label='')
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('null', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# set null value
|
||||
f = MultipleChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='')
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('NULL', ''),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
# explicitly disable
|
||||
f = MultipleChoiceFilter(choices=[('a', 'a')], null_label=None)
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
@override_settings(
|
||||
FILTERS_EMPTY_CHOICE_LABEL='EMPTY LABEL',
|
||||
FILTERS_NULL_CHOICE_LABEL='NULL LABEL',
|
||||
FILTERS_NULL_CHOICE_VALUE='NULL VALUE', )
|
||||
def test_settings_overrides(self):
|
||||
f = ChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', 'EMPTY LABEL'),
|
||||
('NULL VALUE', 'NULL LABEL'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
f = MultipleChoiceFilter(choices=[('a', 'a')], )
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('NULL VALUE', 'NULL LABEL'),
|
||||
('a', 'a'),
|
||||
])
|
||||
|
||||
def test_callable_choices(self):
|
||||
def choices():
|
||||
yield ('a', 'a')
|
||||
yield ('b', 'b')
|
||||
|
||||
f = ChoiceFilter(choices=choices)
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('a', 'a'),
|
||||
('b', 'b'),
|
||||
])
|
||||
|
||||
def test_callable_choices_is_lazy(self):
|
||||
def choices():
|
||||
self.fail('choices should not be called during initialization')
|
||||
ChoiceFilter(choices=choices)
|
||||
|
||||
|
||||
class MultipleChoiceFilterTests(TestCase):
|
||||
|
||||
|
@ -700,6 +737,16 @@ class ModelChoiceFilterTests(TestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
f.field
|
||||
|
||||
@override_settings(
|
||||
FILTERS_EMPTY_CHOICE_LABEL='EMPTY',
|
||||
FILTERS_NULL_CHOICE_VALUE='NULL', )
|
||||
def test_empty_choices(self):
|
||||
f = ModelChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL')
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', 'EMPTY'),
|
||||
('null', 'NULL'),
|
||||
])
|
||||
|
||||
def test_default_field_with_queryset(self):
|
||||
qs = mock.NonCallableMock(spec=[])
|
||||
f = ModelChoiceFilter(queryset=qs)
|
||||
|
@ -742,6 +789,15 @@ class ModelMultipleChoiceFilterTests(TestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
f.field
|
||||
|
||||
@override_settings(
|
||||
FILTERS_EMPTY_CHOICE_LABEL='EMPTY',
|
||||
FILTERS_NULL_CHOICE_VALUE='NULL', )
|
||||
def test_empty_choices(self):
|
||||
f = ModelMultipleChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL')
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('null', 'NULL'),
|
||||
])
|
||||
|
||||
def test_default_field_with_queryset(self):
|
||||
qs = mock.NonCallableMock(spec=[])
|
||||
f = ModelMultipleChoiceFilter(queryset=qs)
|
||||
|
@ -847,6 +903,20 @@ class NumericRangeFilterTests(TestCase):
|
|||
f.filter(qs, value)
|
||||
qs.filter.assert_called_once_with(None__exact=(0, 0))
|
||||
|
||||
def test_filtering_startswith(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
value = mock.Mock(start=20, stop=None)
|
||||
f = NumericRangeFilter()
|
||||
f.filter(qs, value)
|
||||
qs.filter.assert_called_once_with(None__startswith=20)
|
||||
|
||||
def test_filtering_endswith(self):
|
||||
qs = mock.Mock(spec=['filter'])
|
||||
value = mock.Mock(start=None, stop=30)
|
||||
f = NumericRangeFilter()
|
||||
f.filter(qs, value)
|
||||
qs.filter.assert_called_once_with(None__endswith=30)
|
||||
|
||||
|
||||
class RangeFilterTests(TestCase):
|
||||
|
||||
|
@ -1132,6 +1202,14 @@ class AllValuesFilterTests(TestCase):
|
|||
field = f.field
|
||||
self.assertIsInstance(field, forms.ChoiceField)
|
||||
|
||||
def test_empty_value_in_choices(self):
|
||||
f = AllValuesFilter(name='username')
|
||||
f.model = User
|
||||
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
])
|
||||
|
||||
|
||||
class LookupTypesTests(TestCase):
|
||||
def test_custom_lookup_exprs(self):
|
||||
|
@ -1282,7 +1360,7 @@ class OrderingFilterTests(TestCase):
|
|||
fields=(('a', 'c'), ('b', 'd')),
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
self.assertSequenceEqual(list(f.field.choices), (
|
||||
('', '---------'),
|
||||
('a', 'A'),
|
||||
('b', 'B'),
|
||||
|
@ -1293,7 +1371,7 @@ class OrderingFilterTests(TestCase):
|
|||
fields=(('a', 'c'), ('b', 'd')),
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
self.assertSequenceEqual(list(f.field.choices), (
|
||||
('', '---------'),
|
||||
('c', 'C'),
|
||||
('-c', 'C (descending)'),
|
||||
|
@ -1307,7 +1385,7 @@ class OrderingFilterTests(TestCase):
|
|||
field_labels={'a': 'foo'},
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(f.field.choices, (
|
||||
self.assertSequenceEqual(list(f.field.choices), (
|
||||
('', '---------'),
|
||||
('c', 'foo'),
|
||||
('-c', 'foo (descending)'),
|
||||
|
@ -1324,7 +1402,7 @@ class OrderingFilterTests(TestCase):
|
|||
}
|
||||
)
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('username', 'BLABLA'),
|
||||
('-username', 'XYZXYZ'),
|
||||
|
@ -1379,7 +1457,7 @@ class OrderingFilterTests(TestCase):
|
|||
with translation.override('pl'):
|
||||
f = OrderingFilter(fields=['username'])
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('username', 'Nazwa użytkownika'),
|
||||
('-username', 'Nazwa użytkownika (malejąco)'),
|
||||
|
@ -1392,8 +1470,13 @@ class OrderingFilterTests(TestCase):
|
|||
field_labels={'username': 'BLABLA'},
|
||||
)
|
||||
|
||||
self.assertEqual(f.field.choices, [
|
||||
self.assertEqual(list(f.field.choices), [
|
||||
('', '---------'),
|
||||
('username', 'BLABLA'),
|
||||
('-username', 'BLABLA (malejąco)'),
|
||||
])
|
||||
|
||||
def test_help_text(self):
|
||||
# regression test for #756 - the ususal CSV help_text is not relevant to ordering filters.
|
||||
self.assertEqual(OrderingFilter().field.help_text, '')
|
||||
self.assertEqual(OrderingFilter(help_text='a').field.help_text, 'a')
|
||||
|
|
|
@ -4,43 +4,46 @@ 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.filterset import FilterSet
|
||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
|
||||
from django_filters.filters import Filter
|
||||
from django_filters.filters import BooleanFilter
|
||||
from django_filters.filters import CharFilter
|
||||
from django_filters.filters import NumberFilter
|
||||
from django_filters.filters import ChoiceFilter
|
||||
from django_filters.filters import ModelChoiceFilter
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filters import UUIDFilter
|
||||
from django_filters.filters import BaseInFilter
|
||||
from django_filters.filters import BaseRangeFilter
|
||||
from django_filters.filters import DateRangeFilter
|
||||
from django_filters.filters import FilterMethod
|
||||
|
||||
from django_filters.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 User
|
||||
from .models import AdminUser
|
||||
from .models import Article
|
||||
from .models import Book
|
||||
from .models import Profile
|
||||
from .models import Comment
|
||||
from .models import Restaurant
|
||||
from .models import NetworkSetting
|
||||
from .models import SubnetMaskField
|
||||
from .models import Account
|
||||
from .models import BankAccount
|
||||
from .models import Node
|
||||
from .models import DirectedNode
|
||||
from .models import Worker
|
||||
from .models import Business
|
||||
from .models import UUIDTestModel
|
||||
from .models import (
|
||||
Account,
|
||||
AdminUser,
|
||||
Article,
|
||||
BankAccount,
|
||||
Book,
|
||||
Business,
|
||||
Comment,
|
||||
DirectedNode,
|
||||
NetworkSetting,
|
||||
Node,
|
||||
Profile,
|
||||
Restaurant,
|
||||
SubnetMaskField,
|
||||
User,
|
||||
UUIDTestModel,
|
||||
Worker
|
||||
)
|
||||
|
||||
|
||||
def checkItemsEqual(L1, L2):
|
||||
|
@ -530,7 +533,7 @@ class FilterSetClassCreationTests(TestCase):
|
|||
SubnetMaskField: {'filter_class': CharFilter}
|
||||
}
|
||||
|
||||
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
|
||||
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr'])
|
||||
|
||||
def test_custom_declared_field_no_warning(self):
|
||||
class F(FilterSet):
|
||||
|
@ -571,6 +574,21 @@ class FilterSetClassCreationTests(TestCase):
|
|||
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):
|
||||
|
||||
|
@ -679,11 +697,15 @@ class FilterSetTogetherTests(TestCase):
|
|||
('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)
|
||||
self.assertEqual(f.qs.count(), 0)
|
||||
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)
|
||||
|
@ -694,15 +716,31 @@ class FilterSetTogetherTests(TestCase):
|
|||
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)
|
||||
self.assertEqual(f.qs.count(), 0)
|
||||
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):
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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 django_filters.filters import CharFilter
|
||||
from django_filters.filters import ChoiceFilter
|
||||
|
||||
from .models import User, ManagerGroup
|
||||
from .models import Book
|
||||
from .models import STATUS_CHOICES, REGULAR, MANAGER
|
||||
from .models import MANAGER, REGULAR, STATUS_CHOICES, Book, ManagerGroup, User
|
||||
|
||||
|
||||
class FilterSetFormTests(TestCase):
|
||||
|
@ -67,7 +63,7 @@ class FilterSetFormTests(TestCase):
|
|||
self.assertEqual(len(f.fields), 1)
|
||||
self.assertIn('status', f.fields)
|
||||
self.assertSequenceEqual(
|
||||
f.fields['status'].choices,
|
||||
list(f.fields['status'].choices),
|
||||
(('', '---------'), ) + STATUS_CHOICES
|
||||
)
|
||||
|
||||
|
@ -120,7 +116,7 @@ class FilterSetFormTests(TestCase):
|
|||
self.assertIn('status', f.fields)
|
||||
self.assertIn('username', f.fields)
|
||||
self.assertSequenceEqual(
|
||||
f.fields['status'].choices,
|
||||
list(f.fields['status'].choices),
|
||||
STATUS_CHOICES
|
||||
)
|
||||
self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect)
|
||||
|
|
|
@ -1,24 +1,38 @@
|
|||
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django.test import TestCase, override_settings
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.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.utils import (
|
||||
get_field_parts, get_model_field, resolve_field,
|
||||
verbose_field_name, verbose_lookup_expr, label_for_filter
|
||||
)
|
||||
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 User
|
||||
from .models import Article
|
||||
from .models import Book
|
||||
from .models import HiredWorker
|
||||
from .models import Business
|
||||
from .models import (
|
||||
Account,
|
||||
Article,
|
||||
Book,
|
||||
Business,
|
||||
Company,
|
||||
HiredWorker,
|
||||
NetworkSetting,
|
||||
User
|
||||
)
|
||||
|
||||
|
||||
class GetFieldPartsTests(TestCase):
|
||||
|
@ -225,6 +239,10 @@ class VerboseFieldNameTests(TestCase):
|
|||
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')
|
||||
|
@ -233,6 +251,10 @@ class VerboseFieldNameTests(TestCase):
|
|||
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')
|
||||
|
@ -241,6 +263,26 @@ class VerboseFieldNameTests(TestCase):
|
|||
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):
|
||||
|
||||
|
@ -285,3 +327,42 @@ class LabelForFilterTests(TestCase):
|
|||
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.'],
|
||||
})
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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.views import FilterView
|
||||
from django_filters.filterset import FilterSet, filterset_factory
|
||||
from django_filters.views import FilterView
|
||||
|
||||
from .models import Book
|
||||
|
||||
|
@ -47,6 +46,33 @@ class GenericClassBasedViewTests(GenericViewTestCase):
|
|||
for b in ['Ender'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'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'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'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)
|
||||
|
@ -72,6 +98,9 @@ class GenericFunctionalViewTests(GenericViewTestCase):
|
|||
response = self.client.get(self.base_url)
|
||||
for b in ['Ender'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')
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.forms import Select, TextInput
|
||||
from django.test import TestCase
|
||||
from django.forms import TextInput, Select
|
||||
|
||||
from django_filters.widgets import BooleanWidget, QueryArrayWidget
|
||||
from django_filters.widgets import BaseCSVWidget
|
||||
from django_filters.widgets import CSVWidget
|
||||
from django_filters.widgets import RangeWidget
|
||||
from django_filters.widgets import LinkWidget
|
||||
from django_filters.widgets import LookupTypeWidget
|
||||
from django_filters.widgets import (
|
||||
BaseCSVWidget,
|
||||
BooleanWidget,
|
||||
CSVWidget,
|
||||
LinkWidget,
|
||||
LookupTypeWidget,
|
||||
QueryArrayWidget,
|
||||
RangeWidget,
|
||||
SuffixedMultiWidget
|
||||
)
|
||||
|
||||
|
||||
class LookupTypeWidgetTests(TestCase):
|
||||
|
@ -121,6 +124,70 @@ class LinkWidgetTests(TestCase):
|
|||
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):
|
||||
|
@ -183,6 +250,9 @@ class CSVWidgetTests(TestCase):
|
|||
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" />""")
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
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}),
|
||||
url(r'^books-legacy/$', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}),
|
||||
url(r'^books/$', FilterView.as_view(model=Book)),
|
||||
]
|
||||
|
|
38
tox.ini
38
tox.ini
|
@ -1,38 +0,0 @@
|
|||
[tox]
|
||||
envlist =
|
||||
{py27,py33,py34,py35}-django18-restframework{34,35},
|
||||
{py27,py34,py35}-django{19,110}-restframework{34,35},
|
||||
{py27,py34,py35}-djangolatest-restframeworklatest,
|
||||
warnings
|
||||
|
||||
|
||||
[testenv]
|
||||
commands = coverage run --source django_filters ./runtests.py {posargs}
|
||||
setenv =
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
deps =
|
||||
django18: django>=1.8.0,<1.9.0
|
||||
django19: django>=1.9.0,<1.10.0
|
||||
django110: django>=1.10.0,<1.11.0
|
||||
djangolatest: https://github.com/django/django/archive/master.tar.gz
|
||||
restframework34: djangorestframework>=3.4,<3.5
|
||||
restframework35: djangorestframework>=3.5,<3.6
|
||||
restframeworklatest: https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
|
||||
-rrequirements/test-ci.txt
|
||||
|
||||
[testenv:py27-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:py34-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:py35-djangolatest-restframeworklatest]
|
||||
ignore_outcome = True
|
||||
|
||||
[testenv:warnings]
|
||||
ignore_outcome = True
|
||||
commands = python -Werror ./runtests.py {posargs}
|
||||
deps =
|
||||
https://github.com/django/django/archive/master.tar.gz
|
||||
https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
|
||||
-rrequirements/test-ci.txt
|
Loading…
Reference in New Issue