Import django-filter_1.1.0.orig.tar.gz
[dgit import orig django-filter_1.1.0.orig.tar.gz]
This commit is contained in:
commit
50215cbe9c
|
@ -0,0 +1,22 @@
|
|||
Authors
|
||||
=======
|
||||
|
||||
Thanks to the following people for contributing to django-filter.
|
||||
|
||||
Ben Firshman
|
||||
Alex Gaynor
|
||||
Jannis Leidel
|
||||
Martin Mahner
|
||||
Brian Rosner
|
||||
Adam Vandenberg
|
||||
Florian Apolloner
|
||||
Andrew Ball
|
||||
Tino de Bruijn
|
||||
Maximillian Dornseif
|
||||
Marc Fargas
|
||||
Vladimir Sidorenko
|
||||
Tom Christie
|
||||
Remco Wendt
|
||||
Axel Haustant
|
||||
Brad Erickson
|
||||
Diogo Laginha
|
|
@ -0,0 +1,310 @@
|
|||
Version 1.1 (2017-10-19)
|
||||
------------------------
|
||||
|
||||
* Add Deprecations for 2.0 (#792)
|
||||
* Improve IsoDateTimeField test clarity (#790)
|
||||
* Fix form attr references in tests (#789)
|
||||
* Simplify tox config, drop python 3.3 & django 1.8 (#787)
|
||||
* Make get_filter_name a classmethod, allowing it to be overriden for each FilterClass (#775)
|
||||
* Support active timezone (#750)
|
||||
* Docs Typo: django_filters -> filters in docs (#773)
|
||||
* Add Polish translations for some messages (#771)
|
||||
* Remove support for Django 1.9 (EOL) (#752)
|
||||
* Use required attribute from field when getting schema fields (#766)
|
||||
* Prevent circular ImportError hiding for rest_framework sub-package (#741)
|
||||
* Deprecate 'extra' field attrs on Filter (#734)
|
||||
* Add SuffixedMultiWidget (#681)
|
||||
* Fix null filtering for *Choice filters (#680)
|
||||
* Use isort on imports (#761)
|
||||
* Use urlencode from django.utils.http (#760)
|
||||
* Remove OrderingFilter.help_text (#757)
|
||||
* Update DRF test dependency to 3.6 (#747)
|
||||
|
||||
|
||||
Version 1.0.4 (2017-05-19)
|
||||
--------------------------
|
||||
|
||||
Quick fix for verbose_field_name issue from 1.0.3 (#722)
|
||||
|
||||
|
||||
Version 1.0.3 (2017-05-16)
|
||||
--------------------------
|
||||
|
||||
Improves compatibility with Django REST Framework schema generation.
|
||||
|
||||
See the `1.0.3 Milestone`__ for full details.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/milestone/13?closed=1
|
||||
|
||||
|
||||
|
||||
Version 1.0.2 (2017-03-20)
|
||||
--------------------------
|
||||
|
||||
Updates for compatibility with Django 1.11 and Django REST Framework 3.6.
|
||||
|
||||
Adds CI testing against Python 3.6
|
||||
|
||||
See the `1.0.2 Milestone`__ for full details.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/milestone/12?closed=1
|
||||
|
||||
|
||||
Version 1.0.1 (2016-11-28)
|
||||
--------------------------
|
||||
|
||||
Small release to ease compatibility with DRF:
|
||||
|
||||
* #568 Adds ``rest_framework`` to the ``django_filters`` namespace to allow single
|
||||
``import django_filters` usage.
|
||||
* A number of small updates to the docs
|
||||
|
||||
|
||||
Version 1.0 (2016-11-17)
|
||||
------------------------
|
||||
|
||||
This release removes all the deprecated code from 0.14 and 0.15 for 1.0 #480.
|
||||
|
||||
Please see the `Migration Notes`__ for details of how to migrate.
|
||||
Stick with 0.15.3 if you're not ready to update.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt
|
||||
|
||||
The release includes a number of small fixes and documentation updates.
|
||||
|
||||
See the `1.0 Milestone`__ for full details.
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/milestone/8?closed=1
|
||||
|
||||
|
||||
Version 0.15.3 (2016-10-17)
|
||||
---------------------------
|
||||
|
||||
Adds compatibility for DRF (3.5+) get_schema_fields filter backend
|
||||
introspection.
|
||||
|
||||
* #492 Port get_schema_fields from DRF
|
||||
|
||||
|
||||
Version 0.15.2 (2016-09-29)
|
||||
---------------------------
|
||||
|
||||
* #507 Fix compatibility issue when not using the DTL
|
||||
|
||||
|
||||
Version 0.15.1 (2016-09-28)
|
||||
---------------------------
|
||||
|
||||
A couple of quick bug fixes:
|
||||
|
||||
* #496 OrderingFilter not working with Select widget
|
||||
|
||||
* #498 DRF Backend Templates not loading
|
||||
|
||||
|
||||
|
||||
Version 0.15.0 (2016-09-20)
|
||||
---------------------------
|
||||
|
||||
This is a preparatory release for a 1.0. Lots of clean-up, lots of changes,
|
||||
mostly backwards compatible.
|
||||
|
||||
Special thanks to Ryan P Kilby (@rpkilby) for lots of hard work.
|
||||
|
||||
Most changes should raise a Deprecation Warning.
|
||||
|
||||
**Note**: if you're doing *Clever Things™* with the various filter options
|
||||
— ``filter_overrides`` etc — you may run into an `AttributeError` since these
|
||||
are now defined on the metaclass and not on the filter itself.
|
||||
(See the discussion on #459)
|
||||
|
||||
Summary: Highly Recommended, but take a moment to ensure everything still works.
|
||||
|
||||
* Added the DRF backend. #481
|
||||
|
||||
* Deprecated `MethodFilter` in favour of `Filter.method` #382
|
||||
|
||||
* Move filter options to metaclass #459
|
||||
|
||||
* Added `get_filter_predicate` hook. (Allows e.g. filtering on annotated fields) #469
|
||||
|
||||
* Rework Ordering options into a filter #472
|
||||
|
||||
* Hardened all deprecations for 1.0. Please do see the `Migration Notes`__
|
||||
|
||||
__ https://github.com/carltongibson/django-filter/blob/1.0.0/docs/guide/migration.txt
|
||||
|
||||
|
||||
|
||||
Version 0.14.0 (2016-08-14)
|
||||
---------------------------
|
||||
|
||||
* Confirmed support for Django 1.10.
|
||||
|
||||
* Add support for filtering on DurationField (new in Django 1.8).
|
||||
|
||||
* Fix UUIDFilter import issue
|
||||
|
||||
* Improve FieldLookupError message
|
||||
|
||||
* Add filters_for_model to improve extensibility
|
||||
|
||||
* Fix limit_choices_to behavior with callables
|
||||
|
||||
* Fix distinct behavior for range filters
|
||||
|
||||
* Various Minor Clean up issues.
|
||||
|
||||
|
||||
Version 0.13.0 (2016-03-11)
|
||||
---------------------------
|
||||
|
||||
* Add support for filtering by CSV #363
|
||||
|
||||
* Add DateTimeFromToRangeFilter #376
|
||||
|
||||
* Add Chinese translation #359
|
||||
|
||||
* Lots of fixes.
|
||||
|
||||
|
||||
Version 0.12.0 (2016-01-07)
|
||||
---------------------------
|
||||
|
||||
* Raised minimum Django version to 1.8.x
|
||||
|
||||
* FEATURE: Add support for custom ORM lookup types #221
|
||||
|
||||
* FEATURE: Add JavaScript friendly BooleanWidget #270
|
||||
|
||||
* FIXED: (More) Compatability with Django 1.8 and Django 1.9+
|
||||
|
||||
* BREAKING CHANGE: custom filter names are now also be used for ordering #230
|
||||
|
||||
If you use ordering on a field you defined as custom filter with custom
|
||||
name, you should now use the filter name as ordering key as well.
|
||||
|
||||
Eg. For a filter like :
|
||||
|
||||
class F(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['account', 'status']
|
||||
order_by = True
|
||||
|
||||
Before, ordering was like `?o=username`. Since 0.12.0 it's `o=account`.
|
||||
|
||||
|
||||
Version 0.11.0 (2015-08-14)
|
||||
---------------------------
|
||||
|
||||
* FEATURE: Added default filter method lookup for MethodFilter #222
|
||||
|
||||
* FEATURE: Added support for yesterday in daterangefilter #234
|
||||
|
||||
* FEATURE: Created Filter for NumericRange. #236
|
||||
|
||||
* FEATURE: Added Date/time range filters #215
|
||||
|
||||
* FEATURE: Added option to raise with `strict` #255
|
||||
|
||||
* FEATURE: Added Form Field and Filter to parse ISO-8601 timestamps
|
||||
|
||||
|
||||
Version 0.10.0 (2015-05-13)
|
||||
---------------------
|
||||
|
||||
* FEATURE: Added ``conjoined`` parameter to ``MultipleChoiceFilter``
|
||||
|
||||
* FEATURE: Added ``together`` meta option to validate fields as a group
|
||||
|
||||
* FIXED: Added testing on Django 1.8
|
||||
|
||||
* FIXED: ``get_model_field`` on Django 1.8
|
||||
|
||||
|
||||
Version 0.9.2 (2015-01-23)
|
||||
--------------------------
|
||||
|
||||
* FIXED: Compatibility with Django v1.8a1
|
||||
|
||||
Version 0.9.1 (2014-12-03)
|
||||
--------------------------
|
||||
|
||||
* FIXED: Compatibility with Debug Toolbar's versions panel
|
||||
|
||||
Version 0.9 (2014-11-28)
|
||||
------------------------
|
||||
|
||||
* FEATURE: Allow Min/Max-Only use of RangeFilter
|
||||
|
||||
* FEATURE: Added TypedChoiceFilter
|
||||
|
||||
* FIXED: Correct logic for short circuit on MultipleChoiceFilter
|
||||
|
||||
Added `always_filter` attribute and `is_noop()` test to apply short-circuiting.
|
||||
|
||||
Set `always_filter` to `False` on init to apply default `is_noop()` test.
|
||||
Override `is_noop()` for more complex cases.
|
||||
|
||||
* MISC: Version bumping with ``bumpversion``
|
||||
|
||||
|
||||
Version 0.8 (2014-09-29)
|
||||
------------------------
|
||||
|
||||
* FEATURE: Added exclusion filters support
|
||||
|
||||
* FEATURE: Added `fields` dictionary shorthand syntax
|
||||
|
||||
* FEATURE: Added `MethodFilter`.
|
||||
|
||||
* FIXED: #115 "filters.Filter.filter() fails if it receives [] or () as value"
|
||||
|
||||
* MISC: Various Documentation and Testing improvements
|
||||
|
||||
|
||||
|
||||
Version 0.7 (2013-08-10)
|
||||
------------------------
|
||||
|
||||
* FEATURE: Added support for AutoField.
|
||||
|
||||
* FEATURE: There is a "distinct" flag to ensure that only unique rows are
|
||||
returned.
|
||||
|
||||
* FEATURE: Support descending ordering (slighty backwards incompatible).
|
||||
|
||||
* FEATURE: Support "strict" querysets, ie wrong filter data returns no results.
|
||||
|
||||
* FIXED: Some translation strings were changed to be in line with admin.
|
||||
|
||||
* FIXED: Support for Django 1.7.
|
||||
|
||||
Version 0.6 (2013-03-25)
|
||||
------------------------
|
||||
|
||||
* raised minimum Django version to 1.4.x
|
||||
|
||||
* added Python 3.2 and Python 3.3 support
|
||||
|
||||
* added Django 1.5 support and initial 1.6 compatability
|
||||
|
||||
* FEATURE: recognition of custom model field subclasses
|
||||
|
||||
* FEATURE: allow optional display names for order_by values
|
||||
|
||||
* FEATURE: addition of class-based FilterView
|
||||
|
||||
* FEATURE: addition of count() method on FilterSet to prevent pagination
|
||||
from loading entire queryset
|
||||
|
||||
* FIXED: attempts to filter on reverse side of m2m, o2o or fk would
|
||||
raise an error
|
||||
|
||||
|
||||
Version 0.5.4 (2012-11-16)
|
||||
--------------------------
|
||||
|
||||
* project brought back to life
|
|
@ -0,0 +1,24 @@
|
|||
Copyright (c) Alex Gaynor and individual contributors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* The names of its contributors may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,14 @@
|
|||
include AUTHORS
|
||||
include CHANGES.rst
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include runshell.py
|
||||
include runtests.py
|
||||
recursive-include docs *
|
||||
recursive-include requirements *
|
||||
recursive-include tests *
|
||||
recursive-include django_filters/locale *
|
||||
recursive-include django_filters/templates *.html
|
||||
prune docs/_build
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co]
|
|
@ -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,102 @@
|
|||
Django Filter
|
||||
=============
|
||||
|
||||
Django-filter is a reusable Django application allowing users to declaratively
|
||||
add dynamic ``QuerySet`` filtering from URL parameters.
|
||||
|
||||
Full documentation on `read the docs`_.
|
||||
|
||||
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
|
||||
:target: https://travis-ci.org/carltongibson/django-filter
|
||||
|
||||
.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
|
||||
:target: https://codecov.io/gh/carltongibson/django-filter
|
||||
|
||||
.. image:: https://badge.fury.io/py/django-filter.svg
|
||||
:target: http://badge.fury.io/py/django-filter
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* **Python**: 2.7, 3.4, 3.5, 3.6
|
||||
* **Django**: 1.8, 1.10, 1.11
|
||||
* **DRF**: 3.7
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Install using pip:
|
||||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install django-filter
|
||||
|
||||
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Django-filter can be used for generating interfaces similar to the Django
|
||||
admin's ``list_filter`` interface. It has an API very similar to Django's
|
||||
``ModelForms``. For example, if you had a Product model you could have a
|
||||
filterset for it with the code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['name', 'price', 'manufacturer']
|
||||
|
||||
|
||||
And then in your view you could do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def product_list(request):
|
||||
filter = ProductFilter(request.GET, queryset=Product.objects.all())
|
||||
return render(request, 'my_app/template.html', {'filter': filter})
|
||||
|
||||
|
||||
Usage with Django REST Framework
|
||||
--------------------------------
|
||||
|
||||
Django-filter provides a custom ``FilterSet`` and filter backend for use with
|
||||
Django REST Framework.
|
||||
|
||||
To use this adjust your import to use
|
||||
``django_filters.rest_framework.FilterSet``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
class ProductFilter(filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ('category', 'in_stock')
|
||||
|
||||
|
||||
For more details see the `DRF integration docs`_.
|
||||
|
||||
|
||||
Support
|
||||
-------
|
||||
|
||||
If you have questions about usage or development you can join the
|
||||
`mailing list`_.
|
||||
|
||||
.. _`read the docs`: https://django-filter.readthedocs.io/en/develop/
|
||||
.. _`mailing list`: http://groups.google.com/group/django-filter
|
||||
.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html
|
|
@ -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
|
|
@ -0,0 +1,34 @@
|
|||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
|
||||
import pkgutil
|
||||
|
||||
from .constants import STRICTNESS
|
||||
from .filterset import FilterSet
|
||||
from .filters import *
|
||||
|
||||
# We make the `rest_framework` module available without an additional import.
|
||||
# If DRF is not installed, no-op.
|
||||
if pkgutil.find_loader('rest_framework') is not None:
|
||||
from . import rest_framework
|
||||
del pkgutil
|
||||
|
||||
__version__ = '1.1.0'
|
||||
|
||||
|
||||
def parse_version(version):
|
||||
'''
|
||||
'0.1.2-dev' -> (0, 1, 2, 'dev')
|
||||
'0.1.2' -> (0, 1, 2)
|
||||
'''
|
||||
v = version.split('.')
|
||||
v = v[:-1] + v[-1].split('-')
|
||||
ret = []
|
||||
for p in v:
|
||||
if p.isdigit():
|
||||
ret.append(int(p))
|
||||
else:
|
||||
ret.append(p)
|
||||
return tuple(ret)
|
||||
|
||||
VERSION = parse_version(__version__)
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import make_aware as make_aware_orig
|
||||
|
||||
try:
|
||||
from django.forms.utils import pretty_name
|
||||
except ImportError: # Django 1.8
|
||||
from django.forms.forms import pretty_name
|
||||
|
||||
# django-crispy-forms is optional
|
||||
try:
|
||||
import crispy_forms
|
||||
except ImportError:
|
||||
crispy_forms = None
|
||||
|
||||
|
||||
def is_crispy():
|
||||
return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
|
||||
|
||||
|
||||
# coreapi is optional (Note that uritemplate is a dependency of coreapi)
|
||||
# Fixes #525 - cannot simply import from rest_framework.compat, due to
|
||||
# import issues w/ django-guardian.
|
||||
try:
|
||||
import coreapi
|
||||
except ImportError:
|
||||
coreapi = None
|
||||
|
||||
try:
|
||||
import coreschema
|
||||
except ImportError:
|
||||
coreschema = None
|
||||
|
||||
def remote_field(field):
|
||||
"""
|
||||
https://docs.djangoproject.com/en/1.9/releases/1.9/#field-rel-changes
|
||||
"""
|
||||
if django.VERSION >= (1, 9):
|
||||
return field.remote_field
|
||||
return field.rel
|
||||
|
||||
|
||||
def remote_model(field):
|
||||
if django.VERSION >= (1, 9):
|
||||
return remote_field(field).model
|
||||
return remote_field(field).to
|
||||
|
||||
|
||||
def remote_queryset(field):
|
||||
model = remote_model(field)
|
||||
limit_choices_to = field.get_limit_choices_to()
|
||||
|
||||
return model._default_manager.complex_filter(limit_choices_to)
|
||||
|
||||
|
||||
def format_value(widget, value):
|
||||
if django.VERSION >= (1, 10):
|
||||
return widget.format_value(value)
|
||||
return widget._format_value(value)
|
||||
|
||||
|
||||
|
||||
def make_aware(value, timezone, is_dst):
|
||||
"""is_dst was added for 1.9"""
|
||||
if django.VERSION >= (1, 9):
|
||||
return make_aware_orig(value, timezone, is_dst)
|
||||
else:
|
||||
return make_aware_orig(value, timezone)
|
|
@ -0,0 +1,117 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings as dj_settings
|
||||
from django.core.signals import setting_changed
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .constants import STRICTNESS
|
||||
from .utils import deprecate
|
||||
|
||||
DEFAULTS = {
|
||||
'DISABLE_HELP_TEXT': False,
|
||||
'HELP_TEXT_FILTER': True,
|
||||
'HELP_TEXT_EXCLUDE': True,
|
||||
|
||||
# empty/null choices
|
||||
'EMPTY_CHOICE_LABEL': '---------',
|
||||
'NULL_CHOICE_LABEL': None,
|
||||
'NULL_CHOICE_VALUE': 'null',
|
||||
|
||||
'STRICTNESS': STRICTNESS.RETURN_NO_RESULTS,
|
||||
|
||||
'VERBOSE_LOOKUPS': {
|
||||
# transforms don't need to be verbose, since their expressions are chained
|
||||
'date': _('date'),
|
||||
'year': _('year'),
|
||||
'month': _('month'),
|
||||
'day': _('day'),
|
||||
'week_day': _('week day'),
|
||||
'hour': _('hour'),
|
||||
'minute': _('minute'),
|
||||
'second': _('second'),
|
||||
|
||||
# standard lookups
|
||||
'exact': _(''),
|
||||
'iexact': _(''),
|
||||
'contains': _('contains'),
|
||||
'icontains': _('contains'),
|
||||
'in': _('is in'),
|
||||
'gt': _('is greater than'),
|
||||
'gte': _('is greater than or equal to'),
|
||||
'lt': _('is less than'),
|
||||
'lte': _('is less than or equal to'),
|
||||
'startswith': _('starts with'),
|
||||
'istartswith': _('starts with'),
|
||||
'endswith': _('ends with'),
|
||||
'iendswith': _('ends with'),
|
||||
'range': _('is in range'),
|
||||
'isnull': _(''),
|
||||
'regex': _('matches regex'),
|
||||
'iregex': _('matches regex'),
|
||||
'search': _('search'),
|
||||
|
||||
# postgres lookups
|
||||
'contained_by': _('is contained by'),
|
||||
'overlap': _('overlaps'),
|
||||
'has_key': _('has key'),
|
||||
'has_keys': _('has keys'),
|
||||
'has_any_keys': _('has any keys'),
|
||||
'trigram_similar': _('search'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
DEPRECATED_SETTINGS = [
|
||||
'HELP_TEXT_FILTER',
|
||||
'HELP_TEXT_EXCLUDE',
|
||||
]
|
||||
|
||||
|
||||
def is_callable(value):
|
||||
# check for callables, except types
|
||||
return callable(value) and not isinstance(value, type)
|
||||
|
||||
|
||||
class Settings(object):
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name not in DEFAULTS:
|
||||
msg = "'%s' object has no attribute '%s'"
|
||||
raise AttributeError(msg % (self.__class__.__name__, name))
|
||||
|
||||
value = self.get_setting(name)
|
||||
|
||||
if is_callable(value):
|
||||
value = value()
|
||||
|
||||
# Cache the result
|
||||
setattr(self, name, value)
|
||||
return value
|
||||
|
||||
def get_setting(self, setting):
|
||||
django_setting = 'FILTERS_%s' % setting
|
||||
|
||||
if setting in DEPRECATED_SETTINGS and hasattr(dj_settings, django_setting):
|
||||
deprecate("The '%s' setting has been deprecated." % django_setting)
|
||||
|
||||
return getattr(dj_settings, django_setting, DEFAULTS[setting])
|
||||
|
||||
def change_setting(self, setting, value, enter, **kwargs):
|
||||
if not setting.startswith('FILTERS_'):
|
||||
return
|
||||
setting = setting[8:] # strip 'FILTERS_'
|
||||
|
||||
# ensure a valid app setting is being overridden
|
||||
if setting not in DEFAULTS:
|
||||
return
|
||||
|
||||
# if exiting, delete value to repopulate
|
||||
if enter:
|
||||
setattr(self, setting, value)
|
||||
else:
|
||||
delattr(self, setting)
|
||||
|
||||
|
||||
settings = Settings()
|
||||
setting_changed.connect(settings.change_setting)
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
ALL_FIELDS = '__all__'
|
||||
|
||||
|
||||
EMPTY_VALUES = ([], (), {}, '', None)
|
||||
|
||||
|
||||
class STRICTNESS(object):
|
||||
class IGNORE(object):
|
||||
pass
|
||||
|
||||
class RETURN_NO_RESULTS(object):
|
||||
pass
|
||||
|
||||
class RAISE_VALIDATION_ERROR(object):
|
||||
pass
|
||||
|
||||
# Values of False & True chosen for backward compatability reasons.
|
||||
# Originally, these were the only options.
|
||||
_LEGACY = {
|
||||
False: IGNORE,
|
||||
True: RETURN_NO_RESULTS,
|
||||
"RAISE": RAISE_VALIDATION_ERROR,
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
from django.core.exceptions import FieldError
|
||||
|
||||
|
||||
class FieldLookupError(FieldError):
|
||||
def __init__(self, model_field, lookup_expr):
|
||||
super(FieldLookupError, self).__init__(
|
||||
"Unsupported lookup '%s' for field '%s'." % (lookup_expr, model_field)
|
||||
)
|
|
@ -0,0 +1,294 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, time
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .conf import settings
|
||||
from .utils import handle_timezone
|
||||
from .widgets import BaseCSVWidget, CSVWidget, LookupTypeWidget, RangeWidget
|
||||
|
||||
|
||||
class RangeField(forms.MultiValueField):
|
||||
widget = RangeWidget
|
||||
|
||||
def __init__(self, fields=None, *args, **kwargs):
|
||||
if fields is None:
|
||||
fields = (
|
||||
forms.DecimalField(),
|
||||
forms.DecimalField())
|
||||
super(RangeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if data_list:
|
||||
return slice(*data_list)
|
||||
return None
|
||||
|
||||
|
||||
class DateRangeField(RangeField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
fields = (
|
||||
forms.DateField(),
|
||||
forms.DateField())
|
||||
super(DateRangeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if data_list:
|
||||
start_date, stop_date = data_list
|
||||
if start_date:
|
||||
start_date = handle_timezone(
|
||||
datetime.combine(start_date, time.min),
|
||||
False
|
||||
)
|
||||
if stop_date:
|
||||
stop_date = handle_timezone(
|
||||
datetime.combine(stop_date, time.max),
|
||||
False
|
||||
)
|
||||
return slice(start_date, stop_date)
|
||||
return None
|
||||
|
||||
|
||||
class DateTimeRangeField(RangeField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
fields = (
|
||||
forms.DateTimeField(),
|
||||
forms.DateTimeField())
|
||||
super(DateTimeRangeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
|
||||
class TimeRangeField(RangeField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
fields = (
|
||||
forms.TimeField(),
|
||||
forms.TimeField())
|
||||
super(TimeRangeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
|
||||
class Lookup(namedtuple('Lookup', ('value', 'lookup_type'))):
|
||||
# python nature is test __len__ on tuple types for boolean check
|
||||
def __len__(self):
|
||||
if not self.value:
|
||||
return 0
|
||||
return 2
|
||||
|
||||
|
||||
class LookupTypeField(forms.MultiValueField):
|
||||
def __init__(self, field, lookup_choices, *args, **kwargs):
|
||||
fields = (
|
||||
field,
|
||||
forms.ChoiceField(choices=lookup_choices)
|
||||
)
|
||||
defaults = {
|
||||
'widgets': [f.widget for f in fields],
|
||||
}
|
||||
widget = LookupTypeWidget(**defaults)
|
||||
kwargs['widget'] = widget
|
||||
kwargs['help_text'] = field.help_text
|
||||
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
|
||||
|
||||
def compress(self, data_list):
|
||||
if len(data_list) == 2:
|
||||
return Lookup(value=data_list[0], lookup_type=data_list[1] or 'exact')
|
||||
return Lookup(value=None, lookup_type='exact')
|
||||
|
||||
|
||||
class IsoDateTimeField(forms.DateTimeField):
|
||||
"""
|
||||
Supports 'iso-8601' date format too which is out the scope of
|
||||
the ``datetime.strptime`` standard library
|
||||
|
||||
# ISO 8601: ``http://www.w3.org/TR/NOTE-datetime``
|
||||
|
||||
Based on Gist example by David Medina https://gist.github.com/copitux/5773821
|
||||
"""
|
||||
ISO_8601 = 'iso-8601'
|
||||
input_formats = [ISO_8601]
|
||||
|
||||
def strptime(self, value, format):
|
||||
value = force_str(value)
|
||||
|
||||
if format == self.ISO_8601:
|
||||
parsed = parse_datetime(value)
|
||||
if parsed is None: # Continue with other formats if doesn't match
|
||||
raise ValueError
|
||||
return handle_timezone(parsed)
|
||||
return super(IsoDateTimeField, self).strptime(value, format)
|
||||
|
||||
|
||||
class BaseCSVField(forms.Field):
|
||||
"""
|
||||
Base field for validating CSV types. Value validation is performed by
|
||||
secondary base classes.
|
||||
|
||||
ex::
|
||||
class IntegerCSVField(BaseCSVField, filters.IntegerField):
|
||||
pass
|
||||
|
||||
"""
|
||||
base_widget_class = BaseCSVWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
widget = kwargs.get('widget') or self.widget
|
||||
kwargs['widget'] = self._get_widget_class(widget)
|
||||
|
||||
super(BaseCSVField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _get_widget_class(self, widget):
|
||||
# passthrough, allows for override
|
||||
if isinstance(widget, BaseCSVWidget) or (
|
||||
isinstance(widget, type) and
|
||||
issubclass(widget, BaseCSVWidget)):
|
||||
return widget
|
||||
|
||||
# complain since we are unable to reconstruct widget instances
|
||||
assert isinstance(widget, type), \
|
||||
"'%s.widget' must be a widget class, not %s." \
|
||||
% (self.__class__.__name__, repr(widget))
|
||||
|
||||
bases = (self.base_widget_class, widget, )
|
||||
return type(str('CSV%s' % widget.__name__), bases, {})
|
||||
|
||||
def clean(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return [super(BaseCSVField, self).clean(v) for v in value]
|
||||
|
||||
|
||||
class BaseRangeField(BaseCSVField):
|
||||
# Force use of text input, as range must always have two inputs. A date
|
||||
# input would only allow a user to input one value and would always fail.
|
||||
widget = CSVWidget
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_values': _('Range query expects two values.')
|
||||
}
|
||||
|
||||
def clean(self, value):
|
||||
value = super(BaseRangeField, self).clean(value)
|
||||
|
||||
if value is not None and len(value) != 2:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_values'],
|
||||
code='invalid_values')
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ChoiceIterator(object):
|
||||
# Emulates the behavior of ModelChoiceIterator, but instead wraps
|
||||
# the field's _choices iterable.
|
||||
|
||||
def __init__(self, field, choices):
|
||||
self.field = field
|
||||
self.choices = choices
|
||||
|
||||
def __iter__(self):
|
||||
if self.field.empty_label is not None:
|
||||
yield ("", self.field.empty_label)
|
||||
if self.field.null_label is not None:
|
||||
yield (self.field.null_value, self.field.null_label)
|
||||
|
||||
# Python 2 lacks 'yield from'
|
||||
for choice in self.choices:
|
||||
yield choice
|
||||
|
||||
def __len__(self):
|
||||
add = 1 if self.field.empty_label is not None else 0
|
||||
add += 1 if self.field.null_label is not None else 0
|
||||
return len(self.choices) + add
|
||||
|
||||
|
||||
class ModelChoiceIterator(forms.models.ModelChoiceIterator):
|
||||
# Extends the base ModelChoiceIterator to add in 'null' choice handling.
|
||||
# This is a bit verbose since we have to insert the null choice after the
|
||||
# empty choice, but before the remainder of the choices.
|
||||
|
||||
def __iter__(self):
|
||||
iterable = super(ModelChoiceIterator, self).__iter__()
|
||||
|
||||
if self.field.empty_label is not None:
|
||||
yield next(iterable)
|
||||
if self.field.null_label is not None:
|
||||
yield (self.field.null_value, self.field.null_label)
|
||||
|
||||
# Python 2 lacks 'yield from'
|
||||
for value in iterable:
|
||||
yield value
|
||||
|
||||
def __len__(self):
|
||||
add = 1 if self.field.null_label is not None else 0
|
||||
return super(ModelChoiceIterator, self).__len__() + add
|
||||
|
||||
|
||||
class ChoiceIteratorMixin(object):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
|
||||
self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
|
||||
|
||||
super(ChoiceIteratorMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def _get_choices(self):
|
||||
if django.VERSION >= (1, 11):
|
||||
return super(ChoiceIteratorMixin, self)._get_choices()
|
||||
|
||||
# HACK: Django < 1.11 does not allow a custom iterator to be provided.
|
||||
# This code only executes for Model*ChoiceFields.
|
||||
if hasattr(self, '_choices'):
|
||||
return self._choices
|
||||
return self.iterator(self)
|
||||
|
||||
def _set_choices(self, value):
|
||||
super(ChoiceIteratorMixin, self)._set_choices(value)
|
||||
value = self.iterator(self, self._choices)
|
||||
|
||||
self._choices = self.widget.choices = value
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
|
||||
# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
|
||||
class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
|
||||
iterator = ChoiceIterator
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
|
||||
super(ChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
|
||||
iterator = ChoiceIterator
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.empty_label = None
|
||||
super(MultipleChoiceField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
|
||||
iterator = ModelChoiceIterator
|
||||
|
||||
def to_python(self, value):
|
||||
# bypass the queryset value check
|
||||
if self.null_label is not None and value == self.null_value:
|
||||
return value
|
||||
return super(ModelChoiceField, self).to_python(value)
|
||||
|
||||
|
||||
class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
|
||||
iterator = ModelChoiceIterator
|
||||
|
||||
def _check_values(self, value):
|
||||
null = self.null_label is not None and value and self.null_value in value
|
||||
if null: # remove the null value and any potential duplicates
|
||||
value = [v for v in value if v != self.null_value]
|
||||
|
||||
result = list(super(ModelMultipleChoiceField, self)._check_values(value))
|
||||
result += [self.null_value] if null else []
|
||||
return result
|
|
@ -0,0 +1,738 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from datetime import timedelta
|
||||
|
||||
from django import forms
|
||||
from django.db.models import Q
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.sql.constants import QUERY_TERMS
|
||||
from django.utils import six
|
||||
from django.utils.itercompat import is_iterable
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from .compat import pretty_name
|
||||
from .conf import settings
|
||||
from .constants import EMPTY_VALUES
|
||||
from .fields import (
|
||||
BaseCSVField,
|
||||
BaseRangeField,
|
||||
ChoiceField,
|
||||
DateRangeField,
|
||||
DateTimeRangeField,
|
||||
IsoDateTimeField,
|
||||
Lookup,
|
||||
LookupTypeField,
|
||||
ModelChoiceField,
|
||||
ModelMultipleChoiceField,
|
||||
MultipleChoiceField,
|
||||
RangeField,
|
||||
TimeRangeField
|
||||
)
|
||||
from .utils import deprecate, label_for_filter
|
||||
|
||||
__all__ = [
|
||||
'AllValuesFilter',
|
||||
'AllValuesMultipleFilter',
|
||||
'BaseCSVFilter',
|
||||
'BaseInFilter',
|
||||
'BaseRangeFilter',
|
||||
'BooleanFilter',
|
||||
'CharFilter',
|
||||
'ChoiceFilter',
|
||||
'DateFilter',
|
||||
'DateFromToRangeFilter',
|
||||
'DateRangeFilter',
|
||||
'DateTimeFilter',
|
||||
'DateTimeFromToRangeFilter',
|
||||
'DurationFilter',
|
||||
'Filter',
|
||||
'IsoDateTimeFilter',
|
||||
'ModelChoiceFilter',
|
||||
'ModelMultipleChoiceFilter',
|
||||
'MultipleChoiceFilter',
|
||||
'NumberFilter',
|
||||
'NumericRangeFilter',
|
||||
'OrderingFilter',
|
||||
'RangeFilter',
|
||||
'TimeFilter',
|
||||
'TimeRangeFilter',
|
||||
'TypedChoiceFilter',
|
||||
'TypedMultipleChoiceFilter',
|
||||
'UUIDFilter',
|
||||
]
|
||||
|
||||
|
||||
LOOKUP_TYPES = sorted(QUERY_TERMS)
|
||||
|
||||
|
||||
def _extra_attr(attr):
|
||||
fmt = ("The `.%s` attribute has been deprecated in favor of making it accessible "
|
||||
"alongside the other field kwargs. You should now access it as `.extra['%s']`.")
|
||||
|
||||
def fget(self):
|
||||
deprecate(fmt % (attr, attr))
|
||||
return self.extra.get(attr)
|
||||
|
||||
def fset(self, value):
|
||||
deprecate(fmt % (attr, attr))
|
||||
self.extra[attr] = value
|
||||
|
||||
return {'fget': fget, 'fset': fset}
|
||||
|
||||
|
||||
class Filter(object):
|
||||
creation_counter = 0
|
||||
field_class = forms.Field
|
||||
|
||||
def __init__(self, field_name=None, label=None, method=None, lookup_expr='exact',
|
||||
distinct=False, exclude=False, **kwargs):
|
||||
self.field_name = field_name
|
||||
if field_name is None and 'name' in kwargs:
|
||||
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
|
||||
self.field_name = kwargs.pop('name')
|
||||
self.label = label
|
||||
self.method = method
|
||||
self.lookup_expr = lookup_expr
|
||||
self.distinct = distinct
|
||||
self.exclude = exclude
|
||||
|
||||
self.extra = kwargs
|
||||
self.extra.setdefault('required', False)
|
||||
|
||||
self.creation_counter = Filter.creation_counter
|
||||
Filter.creation_counter += 1
|
||||
|
||||
def get_method(self, qs):
|
||||
"""Return filter method based on whether we're excluding
|
||||
or simply filtering.
|
||||
"""
|
||||
return qs.exclude if self.exclude else qs.filter
|
||||
|
||||
def method():
|
||||
"""
|
||||
Filter method needs to be lazily resolved, as it may be dependent on
|
||||
the 'parent' FilterSet.
|
||||
"""
|
||||
def fget(self):
|
||||
return self._method
|
||||
|
||||
def fset(self, value):
|
||||
self._method = value
|
||||
|
||||
# clear existing FilterMethod
|
||||
if isinstance(self.filter, FilterMethod):
|
||||
del self.filter
|
||||
|
||||
# override filter w/ FilterMethod.
|
||||
if value is not None:
|
||||
self.filter = FilterMethod(self)
|
||||
|
||||
return locals()
|
||||
method = property(**method())
|
||||
|
||||
def name():
|
||||
def fget(self):
|
||||
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
|
||||
return self.field_name
|
||||
|
||||
def fset(self, value):
|
||||
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
|
||||
self.field_name = value
|
||||
|
||||
return locals()
|
||||
name = property(**name())
|
||||
|
||||
def label():
|
||||
def fget(self):
|
||||
if self._label is None and hasattr(self, 'parent'):
|
||||
model = self.parent._meta.model
|
||||
self._label = label_for_filter(
|
||||
model, self.field_name, self.lookup_expr, self.exclude
|
||||
)
|
||||
return self._label
|
||||
|
||||
def fset(self, value):
|
||||
self._label = value
|
||||
|
||||
return locals()
|
||||
label = property(**label())
|
||||
|
||||
# deprecated field props
|
||||
widget = property(**_extra_attr('widget'))
|
||||
required = property(**_extra_attr('required'))
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
if not hasattr(self, '_field'):
|
||||
field_kwargs = self.extra.copy()
|
||||
|
||||
if settings.DISABLE_HELP_TEXT:
|
||||
field_kwargs.pop('help_text', None)
|
||||
|
||||
if (self.lookup_expr is None or
|
||||
isinstance(self.lookup_expr, (list, tuple))):
|
||||
|
||||
lookup = []
|
||||
|
||||
for x in LOOKUP_TYPES:
|
||||
if isinstance(x, (list, tuple)) and len(x) == 2:
|
||||
choice = (x[0], x[1])
|
||||
else:
|
||||
choice = (x, x)
|
||||
|
||||
if self.lookup_expr is None:
|
||||
lookup.append(choice)
|
||||
else:
|
||||
if isinstance(x, (list, tuple)) and len(x) == 2:
|
||||
if x[0] in self.lookup_expr:
|
||||
lookup.append(choice)
|
||||
else:
|
||||
if x in self.lookup_expr:
|
||||
lookup.append(choice)
|
||||
|
||||
self._field = LookupTypeField(
|
||||
self.field_class(**field_kwargs), lookup,
|
||||
required=field_kwargs['required'], label=self.label)
|
||||
else:
|
||||
self._field = self.field_class(label=self.label, **field_kwargs)
|
||||
return self._field
|
||||
|
||||
def filter(self, qs, value):
|
||||
if isinstance(value, Lookup):
|
||||
lookup = six.text_type(value.lookup_type)
|
||||
value = value.value
|
||||
else:
|
||||
lookup = self.lookup_expr
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, lookup): value})
|
||||
return qs
|
||||
|
||||
|
||||
class CharFilter(Filter):
|
||||
field_class = forms.CharField
|
||||
|
||||
|
||||
class BooleanFilter(Filter):
|
||||
field_class = forms.NullBooleanField
|
||||
|
||||
|
||||
class ChoiceFilter(Filter):
|
||||
field_class = ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
|
||||
super(ChoiceFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value != self.null_value:
|
||||
return super(ChoiceFilter, self).filter(qs, value)
|
||||
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
class TypedChoiceFilter(Filter):
|
||||
field_class = forms.TypedChoiceField
|
||||
|
||||
|
||||
class UUIDFilter(Filter):
|
||||
field_class = forms.UUIDField
|
||||
|
||||
|
||||
class MultipleChoiceFilter(Filter):
|
||||
"""
|
||||
This filter performs OR(by default) or AND(using conjoined=True) query
|
||||
on the selected options.
|
||||
|
||||
Advanced usage
|
||||
--------------
|
||||
Depending on your application logic, when all or no choices are selected,
|
||||
filtering may be a no-operation. In this case you may wish to avoid the
|
||||
filtering overhead, particularly if using a `distinct` call.
|
||||
|
||||
You can override `get_filter_predicate` to use a custom filter.
|
||||
By default it will use the filter's name for the key, and the value will
|
||||
be the model object - or in case of passing in `to_field_name` the
|
||||
value of that attribute on the model.
|
||||
|
||||
Set `always_filter` to `False` after instantiation to enable the default
|
||||
`is_noop` test. You can override `is_noop` if you need a different test
|
||||
for your application.
|
||||
|
||||
`distinct` defaults to `True` as to-many relationships will generally
|
||||
require this.
|
||||
"""
|
||||
field_class = MultipleChoiceField
|
||||
|
||||
always_filter = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('distinct', True)
|
||||
self.conjoined = kwargs.pop('conjoined', False)
|
||||
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
|
||||
super(MultipleChoiceFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def is_noop(self, qs, value):
|
||||
"""
|
||||
Return `True` to short-circuit unnecessary and potentially slow
|
||||
filtering.
|
||||
"""
|
||||
if self.always_filter:
|
||||
return False
|
||||
|
||||
# A reasonable default for being a noop...
|
||||
if self.extra.get('required') and len(value) == len(self.field.choices):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def filter(self, qs, value):
|
||||
if not value:
|
||||
# Even though not a noop, no point filtering if empty.
|
||||
return qs
|
||||
|
||||
if self.is_noop(qs, value):
|
||||
return qs
|
||||
|
||||
if not self.conjoined:
|
||||
q = Q()
|
||||
for v in set(value):
|
||||
if v == self.null_value:
|
||||
v = None
|
||||
predicate = self.get_filter_predicate(v)
|
||||
if self.conjoined:
|
||||
qs = self.get_method(qs)(**predicate)
|
||||
else:
|
||||
q |= Q(**predicate)
|
||||
|
||||
if not self.conjoined:
|
||||
qs = self.get_method(qs)(q)
|
||||
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
def get_filter_predicate(self, v):
|
||||
try:
|
||||
return {self.field_name: getattr(v, self.field.to_field_name)}
|
||||
except (AttributeError, TypeError):
|
||||
return {self.field_name: v}
|
||||
|
||||
|
||||
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
|
||||
field_class = forms.TypedMultipleChoiceField
|
||||
|
||||
|
||||
class DateFilter(Filter):
|
||||
field_class = forms.DateField
|
||||
|
||||
|
||||
class DateTimeFilter(Filter):
|
||||
field_class = forms.DateTimeField
|
||||
|
||||
|
||||
class IsoDateTimeFilter(DateTimeFilter):
|
||||
"""
|
||||
Uses IsoDateTimeField to support filtering on ISO 8601 formated datetimes.
|
||||
|
||||
For context see:
|
||||
|
||||
* https://code.djangoproject.com/ticket/23448
|
||||
* https://github.com/tomchristie/django-rest-framework/issues/1338
|
||||
* https://github.com/alex/django-filter/pull/264
|
||||
"""
|
||||
field_class = IsoDateTimeField
|
||||
|
||||
|
||||
class TimeFilter(Filter):
|
||||
field_class = forms.TimeField
|
||||
|
||||
|
||||
class DurationFilter(Filter):
|
||||
field_class = forms.DurationField
|
||||
|
||||
|
||||
class QuerySetRequestMixin(object):
|
||||
"""
|
||||
Add callable functionality to filters that support the ``queryset``
|
||||
argument. If the ``queryset`` is callable, then it **must** accept the
|
||||
``request`` object as a single argument.
|
||||
|
||||
This is useful for filtering querysets by properties on the ``request``
|
||||
object, such as the user.
|
||||
|
||||
Example::
|
||||
|
||||
def departments(request):
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
The above example restricts the set of departments to those in the logged-in
|
||||
user's associated company.
|
||||
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.get('queryset')
|
||||
super(QuerySetRequestMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_request(self):
|
||||
try:
|
||||
return self.parent.request
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def get_queryset(self, request):
|
||||
queryset = self.queryset
|
||||
|
||||
if callable(queryset):
|
||||
return queryset(request)
|
||||
return queryset
|
||||
|
||||
@property
|
||||
def field(self):
|
||||
request = self.get_request()
|
||||
queryset = self.get_queryset(request)
|
||||
|
||||
if queryset is not None:
|
||||
self.extra['queryset'] = queryset
|
||||
|
||||
return super(QuerySetRequestMixin, self).field
|
||||
|
||||
|
||||
class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
|
||||
field_class = ModelChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('empty_label', settings.EMPTY_CHOICE_LABEL)
|
||||
super(ModelChoiceFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
|
||||
field_class = ModelMultipleChoiceField
|
||||
|
||||
|
||||
class NumberFilter(Filter):
|
||||
field_class = forms.DecimalField
|
||||
|
||||
|
||||
class NumericRangeFilter(Filter):
|
||||
field_class = RangeField
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
|
||||
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
||||
else:
|
||||
if value.start is not None:
|
||||
qs = self.get_method(qs)(**{'%s__startswith' % self.field_name: value.start})
|
||||
if value.stop is not None:
|
||||
qs = self.get_method(qs)(**{'%s__endswith' % self.field_name: value.stop})
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
|
||||
class RangeFilter(Filter):
|
||||
field_class = RangeField
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
if value.start is not None and value.stop is not None:
|
||||
lookup = '%s__range' % self.field_name
|
||||
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
|
||||
else:
|
||||
if value.start is not None:
|
||||
qs = self.get_method(qs)(**{'%s__gte' % self.field_name: value.start})
|
||||
if value.stop is not None:
|
||||
qs = self.get_method(qs)(**{'%s__lte' % self.field_name: value.stop})
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
|
||||
def _truncate(dt):
|
||||
return dt.date()
|
||||
|
||||
|
||||
class DateRangeFilter(ChoiceFilter):
|
||||
options = {
|
||||
'': (_('Any date'), lambda qs, name: qs),
|
||||
1: (_('Today'), lambda qs, name: qs.filter(**{
|
||||
'%s__year' % name: now().year,
|
||||
'%s__month' % name: now().month,
|
||||
'%s__day' % name: now().day
|
||||
})),
|
||||
2: (_('Past 7 days'), lambda qs, name: qs.filter(**{
|
||||
'%s__gte' % name: _truncate(now() - timedelta(days=7)),
|
||||
'%s__lt' % name: _truncate(now() + timedelta(days=1)),
|
||||
})),
|
||||
3: (_('This month'), lambda qs, name: qs.filter(**{
|
||||
'%s__year' % name: now().year,
|
||||
'%s__month' % name: now().month
|
||||
})),
|
||||
4: (_('This year'), lambda qs, name: qs.filter(**{
|
||||
'%s__year' % name: now().year,
|
||||
})),
|
||||
5: (_('Yesterday'), lambda qs, name: qs.filter(**{
|
||||
'%s__year' % name: now().year,
|
||||
'%s__month' % name: now().month,
|
||||
'%s__day' % name: (now() - timedelta(days=1)).day,
|
||||
})),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['choices'] = [
|
||||
(key, value[0]) for key, value in six.iteritems(self.options)]
|
||||
|
||||
# empty/null choices not relevant
|
||||
kwargs.setdefault('empty_label', None)
|
||||
kwargs.setdefault('null_label', None)
|
||||
super(DateRangeFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def filter(self, qs, value):
|
||||
try:
|
||||
value = int(value)
|
||||
except (ValueError, TypeError):
|
||||
value = ''
|
||||
|
||||
assert value in self.options
|
||||
qs = self.options[value][1](qs, self.field_name)
|
||||
if self.distinct:
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
|
||||
|
||||
class DateFromToRangeFilter(RangeFilter):
|
||||
field_class = DateRangeField
|
||||
|
||||
|
||||
class DateTimeFromToRangeFilter(RangeFilter):
|
||||
field_class = DateTimeRangeField
|
||||
|
||||
|
||||
class TimeRangeFilter(RangeFilter):
|
||||
field_class = TimeRangeField
|
||||
|
||||
|
||||
class AllValuesFilter(ChoiceFilter):
|
||||
@property
|
||||
def field(self):
|
||||
qs = self.model._default_manager.distinct()
|
||||
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
|
||||
self.extra['choices'] = [(o, o) for o in qs]
|
||||
return super(AllValuesFilter, self).field
|
||||
|
||||
|
||||
class AllValuesMultipleFilter(MultipleChoiceFilter):
|
||||
@property
|
||||
def field(self):
|
||||
qs = self.model._default_manager.distinct()
|
||||
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
|
||||
self.extra['choices'] = [(o, o) for o in qs]
|
||||
return super(AllValuesMultipleFilter, self).field
|
||||
|
||||
|
||||
class BaseCSVFilter(Filter):
|
||||
"""
|
||||
Base class for CSV type filters, such as IN and RANGE.
|
||||
"""
|
||||
base_field_class = BaseCSVField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('help_text', _('Multiple values may be separated by commas.'))
|
||||
super(BaseCSVFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
class ConcreteCSVField(self.base_field_class, self.field_class):
|
||||
pass
|
||||
ConcreteCSVField.__name__ = self._field_class_name(
|
||||
self.field_class, self.lookup_expr
|
||||
)
|
||||
|
||||
self.field_class = ConcreteCSVField
|
||||
|
||||
@classmethod
|
||||
def _field_class_name(cls, field_class, lookup_expr):
|
||||
"""
|
||||
Generate a suitable class name for the concrete field class. This is not
|
||||
completely reliable, as not all field class names are of the format
|
||||
<Type>Field.
|
||||
|
||||
ex::
|
||||
|
||||
BaseCSVFilter._field_class_name(DateTimeField, 'year__in')
|
||||
|
||||
returns 'DateTimeYearInField'
|
||||
|
||||
"""
|
||||
# DateTimeField => DateTime
|
||||
type_name = field_class.__name__
|
||||
if type_name.endswith('Field'):
|
||||
type_name = type_name[:-5]
|
||||
|
||||
# year__in => YearIn
|
||||
parts = lookup_expr.split(LOOKUP_SEP)
|
||||
expression_name = ''.join(p.capitalize() for p in parts)
|
||||
|
||||
# DateTimeYearInField
|
||||
return str('%s%sField' % (type_name, expression_name))
|
||||
|
||||
|
||||
class BaseInFilter(BaseCSVFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('lookup_expr', 'in')
|
||||
super(BaseInFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseRangeFilter(BaseCSVFilter):
|
||||
base_field_class = BaseRangeField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('lookup_expr', 'range')
|
||||
super(BaseRangeFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class OrderingFilter(BaseCSVFilter, ChoiceFilter):
|
||||
"""
|
||||
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
|
||||
two additional arguments that are used to build the ordering choices.
|
||||
|
||||
* ``fields`` is a mapping of {model field name: parameter name}. The
|
||||
parameter names are exposed in the choices and mask/alias the field
|
||||
names used in the ``order_by()`` call. Similar to field ``choices``,
|
||||
``fields`` accepts the 'list of two-tuples' syntax that retains order.
|
||||
``fields`` may also just be an iterable of strings. In this case, the
|
||||
field names simply double as the exposed parameter names.
|
||||
|
||||
* ``field_labels`` is an optional argument that allows you to customize
|
||||
the display label for the corresponding parameter. It accepts a mapping
|
||||
of {field name: human readable label}. Keep in mind that the key is the
|
||||
field name, and not the exposed parameter name.
|
||||
|
||||
Additionally, you can just provide your own ``choices`` if you require
|
||||
explicit control over the exposed options. For example, when you might
|
||||
want to disable descending sort options.
|
||||
|
||||
This filter is also CSV-based, and accepts multiple ordering params. The
|
||||
default select widget does not enable the use of this, but it is useful
|
||||
for APIs.
|
||||
|
||||
"""
|
||||
descending_fmt = _('%s (descending)')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
``fields`` may be either a mapping or an iterable.
|
||||
``field_labels`` must be a map of field names to display labels
|
||||
"""
|
||||
fields = kwargs.pop('fields', {})
|
||||
fields = self.normalize_fields(fields)
|
||||
field_labels = kwargs.pop('field_labels', {})
|
||||
|
||||
self.param_map = {v: k for k, v in fields.items()}
|
||||
|
||||
if 'choices' not in kwargs:
|
||||
kwargs['choices'] = self.build_choices(fields, field_labels)
|
||||
|
||||
kwargs.setdefault('label', _('Ordering'))
|
||||
kwargs.setdefault('help_text', '')
|
||||
kwargs.setdefault('null_label', None)
|
||||
super(OrderingFilter, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_ordering_value(self, param):
|
||||
descending = param.startswith('-')
|
||||
param = param[1:] if descending else param
|
||||
field_name = self.param_map.get(param, param)
|
||||
|
||||
return "-%s" % field_name if descending else field_name
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
ordering = [self.get_ordering_value(param) for param in value]
|
||||
return qs.order_by(*ordering)
|
||||
|
||||
@classmethod
|
||||
def normalize_fields(cls, fields):
|
||||
"""
|
||||
Normalize the fields into an ordered map of {field name: param name}
|
||||
"""
|
||||
# fields is a mapping, copy into new OrderedDict
|
||||
if isinstance(fields, dict):
|
||||
return OrderedDict(fields)
|
||||
|
||||
# convert iterable of values => iterable of pairs (field name, param name)
|
||||
assert is_iterable(fields), \
|
||||
"'fields' must be an iterable (e.g., a list, tuple, or mapping)."
|
||||
|
||||
# fields is an iterable of field names
|
||||
assert all(isinstance(field, six.string_types) or
|
||||
is_iterable(field) and len(field) == 2 # may need to be wrapped in parens
|
||||
for field in fields), \
|
||||
"'fields' must contain strings or (field name, param name) pairs."
|
||||
|
||||
return OrderedDict([
|
||||
(f, f) if isinstance(f, six.string_types) else f for f in fields
|
||||
])
|
||||
|
||||
def build_choices(self, fields, labels):
|
||||
ascending = [
|
||||
(param, labels.get(field, _(pretty_name(param))))
|
||||
for field, param in fields.items()
|
||||
]
|
||||
descending = [
|
||||
('-%s' % param, labels.get('-%s' % param, self.descending_fmt % label))
|
||||
for param, label in ascending
|
||||
]
|
||||
|
||||
# interleave the ascending and descending choices
|
||||
return [val for pair in zip(ascending, descending) for val in pair]
|
||||
|
||||
|
||||
class FilterMethod(object):
|
||||
"""
|
||||
This helper is used to override Filter.filter() when a 'method' argument
|
||||
is passed. It proxies the call to the actual method on the filter's parent.
|
||||
"""
|
||||
def __init__(self, filter_instance):
|
||||
self.f = filter_instance
|
||||
|
||||
def __call__(self, qs, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
return self.method(qs, self.f.field_name, value)
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
"""
|
||||
Resolve the method on the parent filterset.
|
||||
"""
|
||||
instance = self.f
|
||||
|
||||
# noop if 'method' is a function
|
||||
if callable(instance.method):
|
||||
return instance.method
|
||||
|
||||
# otherwise, method is the name of a method on the parent FilterSet.
|
||||
assert hasattr(instance, 'parent'), \
|
||||
"Filter '%s' must have a parent FilterSet to find '.%s()'" % \
|
||||
(instance.field_name, instance.method)
|
||||
|
||||
parent = instance.parent
|
||||
method = getattr(parent, instance.method, None)
|
||||
|
||||
assert callable(method), \
|
||||
"Expected parent FilterSet '%s.%s' to have a '.%s()' method." % \
|
||||
(parent.__class__.__module__, parent.__class__.__name__, instance.method)
|
||||
|
||||
return method
|
|
@ -0,0 +1,455 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import copy
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.utils import six
|
||||
|
||||
from .compat import remote_field, remote_queryset
|
||||
from .conf import settings
|
||||
from .constants import ALL_FIELDS, EMPTY_VALUES, STRICTNESS
|
||||
from .filters import (
|
||||
BaseInFilter,
|
||||
BaseRangeFilter,
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
ChoiceFilter,
|
||||
DateFilter,
|
||||
DateTimeFilter,
|
||||
DurationFilter,
|
||||
Filter,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
NumberFilter,
|
||||
TimeFilter,
|
||||
UUIDFilter
|
||||
)
|
||||
from .utils import (
|
||||
deprecate,
|
||||
get_all_model_fields,
|
||||
get_model_field,
|
||||
resolve_field,
|
||||
try_dbfield
|
||||
)
|
||||
|
||||
|
||||
def _together_valid(form, fieldset):
|
||||
field_presence = [
|
||||
form.cleaned_data.get(field) not in EMPTY_VALUES
|
||||
for field in fieldset
|
||||
]
|
||||
|
||||
if any(field_presence):
|
||||
return all(field_presence)
|
||||
return True
|
||||
|
||||
|
||||
def get_full_clean_override(together):
|
||||
# coerce together to list of pairs
|
||||
if isinstance(together[0], (six.string_types)):
|
||||
together = [together]
|
||||
|
||||
def full_clean(form):
|
||||
super(form.__class__, form).full_clean()
|
||||
message = 'Following fields must be together: %s'
|
||||
|
||||
for each in together:
|
||||
if not _together_valid(form, each):
|
||||
return form.add_error(None, message % ','.join(each))
|
||||
|
||||
return full_clean
|
||||
|
||||
|
||||
class FilterSetOptions(object):
|
||||
def __init__(self, options=None):
|
||||
self.model = getattr(options, 'model', None)
|
||||
self.fields = getattr(options, 'fields', None)
|
||||
self.exclude = getattr(options, 'exclude', None)
|
||||
|
||||
self.filter_overrides = getattr(options, 'filter_overrides', {})
|
||||
|
||||
self.strict = getattr(options, 'strict', None)
|
||||
|
||||
self.form = getattr(options, 'form', forms.Form)
|
||||
|
||||
if hasattr(options, 'together'):
|
||||
deprecate('The `Meta.together` option has been deprecated in favor of overriding `Form.clean`.', 1)
|
||||
self.together = getattr(options, 'together', None)
|
||||
|
||||
|
||||
class FilterSetMetaclass(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
attrs['declared_filters'] = cls.get_declared_filters(bases, attrs)
|
||||
|
||||
new_class = super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs)
|
||||
new_class._meta = FilterSetOptions(getattr(new_class, 'Meta', None))
|
||||
new_class.base_filters = new_class.get_filters()
|
||||
|
||||
return new_class
|
||||
|
||||
@classmethod
|
||||
def get_declared_filters(cls, bases, attrs):
|
||||
filters = [
|
||||
(filter_name, attrs.pop(filter_name))
|
||||
for filter_name, obj in list(attrs.items())
|
||||
if isinstance(obj, Filter)
|
||||
]
|
||||
|
||||
# Default the `filter.field_name` to the attribute name on the filterset
|
||||
for filter_name, f in filters:
|
||||
if getattr(f, 'field_name', None) is None:
|
||||
f.field_name = filter_name
|
||||
|
||||
filters.sort(key=lambda x: x[1].creation_counter)
|
||||
|
||||
# merge declared filters from base classes
|
||||
for base in reversed(bases):
|
||||
if hasattr(base, 'declared_filters'):
|
||||
filters = [
|
||||
(name, f) for name, f
|
||||
in base.declared_filters.items()
|
||||
if name not in attrs
|
||||
] + filters
|
||||
|
||||
return OrderedDict(filters)
|
||||
|
||||
|
||||
FILTER_FOR_DBFIELD_DEFAULTS = {
|
||||
models.AutoField: {'filter_class': NumberFilter},
|
||||
models.CharField: {'filter_class': CharFilter},
|
||||
models.TextField: {'filter_class': CharFilter},
|
||||
models.BooleanField: {'filter_class': BooleanFilter},
|
||||
models.DateField: {'filter_class': DateFilter},
|
||||
models.DateTimeField: {'filter_class': DateTimeFilter},
|
||||
models.TimeField: {'filter_class': TimeFilter},
|
||||
models.DurationField: {'filter_class': DurationFilter},
|
||||
models.DecimalField: {'filter_class': NumberFilter},
|
||||
models.SmallIntegerField: {'filter_class': NumberFilter},
|
||||
models.IntegerField: {'filter_class': NumberFilter},
|
||||
models.PositiveIntegerField: {'filter_class': NumberFilter},
|
||||
models.PositiveSmallIntegerField: {'filter_class': NumberFilter},
|
||||
models.FloatField: {'filter_class': NumberFilter},
|
||||
models.NullBooleanField: {'filter_class': BooleanFilter},
|
||||
models.SlugField: {'filter_class': CharFilter},
|
||||
models.EmailField: {'filter_class': CharFilter},
|
||||
models.FilePathField: {'filter_class': CharFilter},
|
||||
models.URLField: {'filter_class': CharFilter},
|
||||
models.GenericIPAddressField: {'filter_class': CharFilter},
|
||||
models.CommaSeparatedIntegerField: {'filter_class': CharFilter},
|
||||
models.UUIDField: {'filter_class': UUIDFilter},
|
||||
models.OneToOneField: {
|
||||
'filter_class': ModelChoiceFilter,
|
||||
'extra': lambda f: {
|
||||
'queryset': remote_queryset(f),
|
||||
'to_field_name': remote_field(f).field_name,
|
||||
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
|
||||
}
|
||||
},
|
||||
models.ForeignKey: {
|
||||
'filter_class': ModelChoiceFilter,
|
||||
'extra': lambda f: {
|
||||
'queryset': remote_queryset(f),
|
||||
'to_field_name': remote_field(f).field_name,
|
||||
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
|
||||
}
|
||||
},
|
||||
models.ManyToManyField: {
|
||||
'filter_class': ModelMultipleChoiceFilter,
|
||||
'extra': lambda f: {
|
||||
'queryset': remote_queryset(f),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class BaseFilterSet(object):
|
||||
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
|
||||
|
||||
def __init__(self, data=None, queryset=None, prefix=None, strict=None, request=None):
|
||||
self.is_bound = data is not None
|
||||
self.data = data or {}
|
||||
if queryset is None:
|
||||
queryset = self._meta.model._default_manager.all()
|
||||
self.queryset = queryset
|
||||
self.form_prefix = prefix
|
||||
|
||||
# What to do on on validation errors
|
||||
# Fallback to meta, then settings strictness
|
||||
if strict is None:
|
||||
strict = self._meta.strict
|
||||
if strict is None:
|
||||
strict = settings.STRICTNESS
|
||||
|
||||
# transform legacy values
|
||||
self.strict = STRICTNESS._LEGACY.get(strict, strict)
|
||||
|
||||
self.request = request
|
||||
|
||||
self.filters = copy.deepcopy(self.base_filters)
|
||||
|
||||
for filter_ in self.filters.values():
|
||||
# propagate the model and filterset to the filters
|
||||
filter_.model = self._meta.model
|
||||
filter_.parent = self
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
if not hasattr(self, '_qs'):
|
||||
if not self.is_bound:
|
||||
self._qs = self.queryset.all()
|
||||
return self._qs
|
||||
|
||||
if not self.form.is_valid():
|
||||
if self.strict == STRICTNESS.RAISE_VALIDATION_ERROR:
|
||||
raise forms.ValidationError(self.form.errors)
|
||||
elif self.strict == STRICTNESS.RETURN_NO_RESULTS:
|
||||
self._qs = self.queryset.none()
|
||||
return self._qs
|
||||
# else STRICTNESS.IGNORE... ignoring
|
||||
|
||||
# start with all the results and filter from there
|
||||
qs = self.queryset.all()
|
||||
for name, filter_ in six.iteritems(self.filters):
|
||||
value = self.form.cleaned_data.get(name)
|
||||
|
||||
if value is not None: # valid & clean data
|
||||
qs = filter_.filter(qs, value)
|
||||
|
||||
self._qs = qs
|
||||
|
||||
return self._qs
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
if not hasattr(self, '_form'):
|
||||
fields = OrderedDict([
|
||||
(name, filter_.field)
|
||||
for name, filter_ in six.iteritems(self.filters)])
|
||||
|
||||
Form = type(str('%sForm' % self.__class__.__name__),
|
||||
(self._meta.form,), fields)
|
||||
if self._meta.together:
|
||||
Form.full_clean = get_full_clean_override(self._meta.together)
|
||||
if self.is_bound:
|
||||
self._form = Form(self.data, prefix=self.form_prefix)
|
||||
else:
|
||||
self._form = Form(prefix=self.form_prefix)
|
||||
return self._form
|
||||
|
||||
@classmethod
|
||||
def get_fields(cls):
|
||||
"""
|
||||
Resolve the 'fields' argument that should be used for generating filters on the
|
||||
filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'.
|
||||
"""
|
||||
model = cls._meta.model
|
||||
fields = cls._meta.fields
|
||||
exclude = cls._meta.exclude
|
||||
|
||||
assert not (fields is None and exclude is None), \
|
||||
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " \
|
||||
"has been deprecated since 0.15.0 and is now disallowed. Add an explicit " \
|
||||
"'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__
|
||||
|
||||
# Setting exclude with no fields implies all other fields.
|
||||
if exclude is not None and fields is None:
|
||||
fields = ALL_FIELDS
|
||||
|
||||
# Resolve ALL_FIELDS into all fields for the filterset's model.
|
||||
if fields == ALL_FIELDS:
|
||||
fields = get_all_model_fields(model)
|
||||
|
||||
# Remove excluded fields
|
||||
exclude = exclude or []
|
||||
if not isinstance(fields, dict):
|
||||
fields = [(f, ['exact']) for f in fields if f not in exclude]
|
||||
else:
|
||||
fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude]
|
||||
|
||||
return OrderedDict(fields)
|
||||
|
||||
@classmethod
|
||||
def get_filter_name(cls, field_name, lookup_expr):
|
||||
"""
|
||||
Combine a field name and lookup expression into a usable filter name.
|
||||
Exact lookups are the implicit default, so "exact" is stripped from the
|
||||
end of the filter name.
|
||||
"""
|
||||
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
|
||||
|
||||
# This also works with transformed exact lookups, such as 'date__exact'
|
||||
_exact = LOOKUP_SEP + 'exact'
|
||||
if filter_name.endswith(_exact):
|
||||
filter_name = filter_name[:-len(_exact)]
|
||||
|
||||
return filter_name
|
||||
|
||||
@classmethod
|
||||
def get_filters(cls):
|
||||
"""
|
||||
Get all filters for the filterset. This is the combination of declared and
|
||||
generated filters.
|
||||
"""
|
||||
|
||||
# No model specified - skip filter generation
|
||||
if not cls._meta.model:
|
||||
return cls.declared_filters.copy()
|
||||
|
||||
# Determine the filters that should be included on the filterset.
|
||||
filters = OrderedDict()
|
||||
fields = cls.get_fields()
|
||||
undefined = []
|
||||
|
||||
for field_name, lookups in fields.items():
|
||||
field = get_model_field(cls._meta.model, field_name)
|
||||
|
||||
# warn if the field doesn't exist.
|
||||
if field is None:
|
||||
undefined.append(field_name)
|
||||
|
||||
# ForeignObjectRel does not support non-exact lookups
|
||||
if isinstance(field, ForeignObjectRel):
|
||||
filters[field_name] = cls.filter_for_reverse_field(field, field_name)
|
||||
continue
|
||||
|
||||
for lookup_expr in lookups:
|
||||
filter_name = cls.get_filter_name(field_name, lookup_expr)
|
||||
|
||||
# If the filter is explicitly declared on the class, skip generation
|
||||
if filter_name in cls.declared_filters:
|
||||
filters[filter_name] = cls.declared_filters[filter_name]
|
||||
continue
|
||||
|
||||
if field is not None:
|
||||
filters[filter_name] = cls.filter_for_field(field, field_name, lookup_expr)
|
||||
|
||||
# filter out declared filters
|
||||
undefined = [f for f in undefined if f not in cls.declared_filters]
|
||||
if undefined:
|
||||
raise TypeError(
|
||||
"'Meta.fields' contains fields that are not defined on this FilterSet: "
|
||||
"%s" % ', '.join(undefined)
|
||||
)
|
||||
|
||||
# Add in declared filters. This is necessary since we don't enforce adding
|
||||
# declared filters to the 'Meta.fields' option
|
||||
filters.update(cls.declared_filters)
|
||||
return filters
|
||||
|
||||
@classmethod
|
||||
def filter_for_field(cls, f, field_name, lookup_expr='exact'):
|
||||
f, lookup_type = resolve_field(f, lookup_expr)
|
||||
|
||||
default = {
|
||||
'field_name': field_name,
|
||||
'lookup_expr': lookup_expr,
|
||||
}
|
||||
|
||||
filter_class, params = cls.filter_for_lookup(f, lookup_type)
|
||||
default.update(params)
|
||||
|
||||
assert filter_class is not None, (
|
||||
"%s resolved field '%s' with '%s' lookup to an unrecognized field "
|
||||
"type %s. Try adding an override to 'Meta.filter_overrides'. See: "
|
||||
"https://django-filter.readthedocs.io/en/develop/ref/filterset.html#customise-filter-generation-with-filter-overrides"
|
||||
) % (cls.__name__, field_name, lookup_expr, f.__class__.__name__)
|
||||
|
||||
return filter_class(**default)
|
||||
|
||||
@classmethod
|
||||
def filter_for_reverse_field(cls, f, field_name):
|
||||
rel = remote_field(f.field)
|
||||
queryset = f.field.model._default_manager.all()
|
||||
default = {
|
||||
'field_name': field_name,
|
||||
'queryset': queryset,
|
||||
}
|
||||
if rel.multiple:
|
||||
return ModelMultipleChoiceFilter(**default)
|
||||
else:
|
||||
return ModelChoiceFilter(**default)
|
||||
|
||||
@classmethod
|
||||
def filter_for_lookup(cls, f, lookup_type):
|
||||
DEFAULTS = dict(cls.FILTER_DEFAULTS)
|
||||
if hasattr(cls, '_meta'):
|
||||
DEFAULTS.update(cls._meta.filter_overrides)
|
||||
|
||||
data = try_dbfield(DEFAULTS.get, f.__class__) or {}
|
||||
filter_class = data.get('filter_class')
|
||||
params = data.get('extra', lambda f: {})(f)
|
||||
|
||||
# if there is no filter class, exit early
|
||||
if not filter_class:
|
||||
return None, {}
|
||||
|
||||
# perform lookup specific checks
|
||||
if lookup_type == 'exact' and f.choices:
|
||||
return ChoiceFilter, {'choices': f.choices}
|
||||
|
||||
if lookup_type == 'isnull':
|
||||
data = try_dbfield(DEFAULTS.get, models.BooleanField)
|
||||
|
||||
filter_class = data.get('filter_class')
|
||||
params = data.get('extra', lambda f: {})(f)
|
||||
return filter_class, params
|
||||
|
||||
if lookup_type == 'in':
|
||||
class ConcreteInFilter(BaseInFilter, filter_class):
|
||||
pass
|
||||
ConcreteInFilter.__name__ = cls._csv_filter_class_name(
|
||||
filter_class, lookup_type
|
||||
)
|
||||
|
||||
return ConcreteInFilter, params
|
||||
|
||||
if lookup_type == 'range':
|
||||
class ConcreteRangeFilter(BaseRangeFilter, filter_class):
|
||||
pass
|
||||
ConcreteRangeFilter.__name__ = cls._csv_filter_class_name(
|
||||
filter_class, lookup_type
|
||||
)
|
||||
|
||||
return ConcreteRangeFilter, params
|
||||
|
||||
return filter_class, params
|
||||
|
||||
@classmethod
|
||||
def _csv_filter_class_name(cls, filter_class, lookup_type):
|
||||
"""
|
||||
Generate a suitable class name for a concrete filter class. This is not
|
||||
completely reliable, as not all filter class names are of the format
|
||||
<Type>Filter.
|
||||
|
||||
ex::
|
||||
|
||||
FilterSet._csv_filter_class_name(DateTimeFilter, 'in')
|
||||
|
||||
returns 'DateTimeInFilter'
|
||||
|
||||
"""
|
||||
# DateTimeFilter => DateTime
|
||||
type_name = filter_class.__name__
|
||||
if type_name.endswith('Filter'):
|
||||
type_name = type_name[:-6]
|
||||
|
||||
# in => In
|
||||
lookup_name = lookup_type.capitalize()
|
||||
|
||||
# DateTimeInFilter
|
||||
return str('%s%sFilter' % (type_name, lookup_name))
|
||||
|
||||
|
||||
class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
|
||||
pass
|
||||
|
||||
|
||||
def filterset_factory(model, fields=ALL_FIELDS):
|
||||
meta = type(str('Meta'), (object,), {'model': model, 'fields': fields})
|
||||
filterset = type(str('%sFilterSet' % model._meta.object_name),
|
||||
(FilterSet,), {'Meta': meta})
|
||||
return filterset
|
Binary file not shown.
|
@ -0,0 +1,48 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-filter\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-08-10 05:34-0500\n"
|
||||
"PO-Revision-Date: 2013-08-10 12:29+0100\n"
|
||||
"Last-Translator: Florian Apolloner <florian@apolloner.eu>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.5.4\n"
|
||||
|
||||
#: filters.py:153
|
||||
msgid "Any date"
|
||||
msgstr "Alle Daten"
|
||||
|
||||
#: filters.py:154
|
||||
msgid "Today"
|
||||
msgstr "Heute"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Past 7 days"
|
||||
msgstr "Letzte 7 Tage"
|
||||
|
||||
#: filters.py:163
|
||||
msgid "This month"
|
||||
msgstr "Diesen Monat"
|
||||
|
||||
#: filters.py:167
|
||||
msgid "This year"
|
||||
msgstr "Dieses Jahr"
|
||||
|
||||
#: filterset.py:332 filterset.py:341
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (absteigend)"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
Binary file not shown.
|
@ -0,0 +1,47 @@
|
|||
# Django Filter translation.
|
||||
# Copyright (C) 2013
|
||||
# This file is distributed under the same license as the django_filter package.
|
||||
# Gonzalo Bustos, 2015.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
|
||||
"PO-Revision-Date: 2015-10-11 20:53-0300\n"
|
||||
"Last-Translator: Gonzalo Bustos\n"
|
||||
"Language-Team: Spanish (Argentina)\n"
|
||||
"Language: es_AR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 1.6.10\n"
|
||||
|
||||
#: filters.py:51
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "Este es un filtro de exclusión"
|
||||
|
||||
#: filters.py:158
|
||||
msgid "Any date"
|
||||
msgstr "Cualquier fecha"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Today"
|
||||
msgstr "Hoy"
|
||||
|
||||
#: filters.py:164
|
||||
msgid "Past 7 days"
|
||||
msgstr "Últimos 7 días"
|
||||
|
||||
#: filters.py:168
|
||||
msgid "This month"
|
||||
msgstr "Este mes"
|
||||
|
||||
#: filters.py:172
|
||||
msgid "This year"
|
||||
msgstr "Este año"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Todos"
|
Binary file not shown.
|
@ -0,0 +1,185 @@
|
|||
# Django Filter translation.
|
||||
# Copyright (C) 2013
|
||||
# This file is distributed under the same license as the django_filter package.
|
||||
# Carlos Goce, 2017.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-01-26 20:32+0100\n"
|
||||
"PO-Revision-Date: 2017-01-26 20:52+0100\n"
|
||||
"Last-Translator: Carlos Goce\n"
|
||||
"Language-Team: Spanish (España)\n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.8.11\n"
|
||||
|
||||
#: conf.py:26
|
||||
msgid "date"
|
||||
msgstr "fecha"
|
||||
|
||||
#: conf.py:27
|
||||
msgid "year"
|
||||
msgstr "año"
|
||||
|
||||
#: conf.py:28
|
||||
msgid "month"
|
||||
msgstr "mes"
|
||||
|
||||
#: conf.py:29
|
||||
msgid "day"
|
||||
msgstr "día"
|
||||
|
||||
#: conf.py:30
|
||||
msgid "week day"
|
||||
msgstr "día de la semana"
|
||||
|
||||
#: conf.py:31
|
||||
msgid "hour"
|
||||
msgstr "hora"
|
||||
|
||||
#: conf.py:32
|
||||
msgid "minute"
|
||||
msgstr "minuto"
|
||||
|
||||
#: conf.py:33
|
||||
msgid "second"
|
||||
msgstr "segundo"
|
||||
|
||||
#: conf.py:38 conf.py:39
|
||||
msgid "contains"
|
||||
msgstr "contiene"
|
||||
|
||||
#: conf.py:40
|
||||
msgid "is in"
|
||||
msgstr "presente en"
|
||||
|
||||
#: conf.py:41
|
||||
msgid "is greater than"
|
||||
msgstr "mayor que"
|
||||
|
||||
#: conf.py:42
|
||||
msgid "is greater than or equal to"
|
||||
msgstr "mayor o igual que"
|
||||
|
||||
#: conf.py:43
|
||||
msgid "is less than"
|
||||
msgstr "menor que"
|
||||
|
||||
#: conf.py:44
|
||||
msgid "is less than or equal to"
|
||||
msgstr "menor o igual que"
|
||||
|
||||
#: conf.py:45 conf.py:46
|
||||
msgid "starts with"
|
||||
msgstr "comienza por"
|
||||
|
||||
#: conf.py:47 conf.py:48
|
||||
msgid "ends with"
|
||||
msgstr "termina por"
|
||||
|
||||
#: conf.py:49
|
||||
msgid "is in range"
|
||||
msgstr "en el rango"
|
||||
|
||||
#: conf.py:51 conf.py:52
|
||||
msgid "matches regex"
|
||||
msgstr "coincide con la expresión regular"
|
||||
|
||||
#: conf.py:53 conf.py:61
|
||||
msgid "search"
|
||||
msgstr "buscar"
|
||||
|
||||
#: conf.py:56
|
||||
msgid "is contained by"
|
||||
msgstr "contenido en"
|
||||
|
||||
#: conf.py:57
|
||||
msgid "overlaps"
|
||||
msgstr "solapado"
|
||||
|
||||
#: conf.py:58
|
||||
msgid "has key"
|
||||
msgstr "contiene la clave"
|
||||
|
||||
#: conf.py:59
|
||||
msgid "has keys"
|
||||
msgstr "contiene las claves"
|
||||
|
||||
#: conf.py:60
|
||||
msgid "has any keys"
|
||||
msgstr "contiene alguna de las claves"
|
||||
|
||||
#: fields.py:167
|
||||
msgid "Range query expects two values."
|
||||
msgstr "Consultar un rango requiere dos valores"
|
||||
|
||||
#: filters.py:443
|
||||
msgid "Any date"
|
||||
msgstr "Cualquier fecha"
|
||||
|
||||
#: filters.py:444
|
||||
msgid "Today"
|
||||
msgstr "Hoy"
|
||||
|
||||
#: filters.py:449
|
||||
msgid "Past 7 days"
|
||||
msgstr "Últimos 7 días"
|
||||
|
||||
#: filters.py:453
|
||||
msgid "This month"
|
||||
msgstr "Este mes"
|
||||
|
||||
#: filters.py:457
|
||||
msgid "This year"
|
||||
msgstr "Este año"
|
||||
|
||||
#: filters.py:460
|
||||
msgid "Yesterday"
|
||||
msgstr "Ayer"
|
||||
|
||||
#: filters.py:526
|
||||
msgid "Multiple values may be separated by commas."
|
||||
msgstr "Múltiples valores separados por comas."
|
||||
|
||||
#: filters.py:605
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (descendente)"
|
||||
|
||||
#: filters.py:621
|
||||
msgid "Ordering"
|
||||
msgstr "Ordenado"
|
||||
|
||||
#: utils.py:220
|
||||
msgid "exclude"
|
||||
msgstr "excluye"
|
||||
|
||||
#: widgets.py:71
|
||||
msgid "All"
|
||||
msgstr "Todo"
|
||||
|
||||
#: widgets.py:119
|
||||
msgid "Unknown"
|
||||
msgstr "Desconocido"
|
||||
|
||||
#: widgets.py:120
|
||||
msgid "Yes"
|
||||
msgstr "Sí"
|
||||
|
||||
#: widgets.py:121
|
||||
msgid "No"
|
||||
msgstr "No"
|
||||
|
||||
#: rest_framework/filterset.py:31
|
||||
#: templates/django_filters/rest_framework/form.html:5
|
||||
msgid "Submit"
|
||||
msgstr "Enviar"
|
||||
|
||||
#: templates/django_filters/rest_framework/crispy_form.html:4
|
||||
#: templates/django_filters/rest_framework/form.html:2
|
||||
msgid "Field filters"
|
||||
msgstr "Filtros de campo"
|
Binary file not shown.
|
@ -0,0 +1,47 @@
|
|||
# Django Filter translation.
|
||||
# Copyright (C) 2013
|
||||
# This file is distributed under the same license as the django_filter package.
|
||||
# Axel Haustant <noirbizarre@gmail.com>, 2013.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-07-05 19:24+0200\n"
|
||||
"PO-Revision-Date: 2013-07-05 19:24+0200\n"
|
||||
"Last-Translator: Axel Haustant <noirbizarre@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: filters.py:51
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "Ceci est un filtre d'exclusion"
|
||||
|
||||
#: filters.py:158
|
||||
msgid "Any date"
|
||||
msgstr "Toutes les dates"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Today"
|
||||
msgstr "Aujourd'hui"
|
||||
|
||||
#: filters.py:164
|
||||
msgid "Past 7 days"
|
||||
msgstr "7 derniers jours"
|
||||
|
||||
#: filters.py:168
|
||||
msgid "This month"
|
||||
msgstr "Ce mois-ci"
|
||||
|
||||
#: filters.py:172
|
||||
msgid "This year"
|
||||
msgstr "Cette année"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Tous"
|
Binary file not shown.
|
@ -0,0 +1,201 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#: conf.py:35 conf.py:36 conf.py:49
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django_filters 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-09-01 17:21+0000\n"
|
||||
"PO-Revision-Date: 2015-07-25 01:27+0100\n"
|
||||
"Last-Translator: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
|
||||
"Language-Team: Adam Dobrawy <naczelnik@jawnosc.tk>\n"
|
||||
"Language: pl_PL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 1.5.4\n"
|
||||
|
||||
#: conf.py:25
|
||||
#, fuzzy
|
||||
#| msgid "Any date"
|
||||
msgid "date"
|
||||
msgstr "Dowolna data"
|
||||
|
||||
#: conf.py:26
|
||||
#, fuzzy
|
||||
#| msgid "This year"
|
||||
msgid "year"
|
||||
msgstr "Ten rok"
|
||||
|
||||
#: conf.py:27
|
||||
#, fuzzy
|
||||
#| msgid "This month"
|
||||
msgid "month"
|
||||
msgstr "Ten miesiąc"
|
||||
|
||||
#: conf.py:28
|
||||
#, fuzzy
|
||||
#| msgid "Today"
|
||||
msgid "day"
|
||||
msgstr "Dziś"
|
||||
|
||||
#: conf.py:29
|
||||
msgid "week day"
|
||||
msgstr "dzień tygodnia"
|
||||
|
||||
#: conf.py:30
|
||||
msgid "hour"
|
||||
msgstr "godzina"
|
||||
|
||||
#: conf.py:31
|
||||
msgid "minute"
|
||||
msgstr "minuta"
|
||||
|
||||
#: conf.py:32
|
||||
msgid "second"
|
||||
msgstr ""
|
||||
|
||||
#: conf.py:37 conf.py:38
|
||||
msgid "contains"
|
||||
msgstr "zawiera"
|
||||
|
||||
#: conf.py:39
|
||||
msgid "is in"
|
||||
msgstr "zawiera się w"
|
||||
|
||||
#: conf.py:40
|
||||
msgid "is greater than"
|
||||
msgstr "powyżej"
|
||||
|
||||
#: conf.py:41
|
||||
msgid "is greater than or equal to"
|
||||
msgstr "powyżej lub równe"
|
||||
|
||||
#: conf.py:42
|
||||
msgid "is less than"
|
||||
msgstr "poniżej"
|
||||
|
||||
#: conf.py:43
|
||||
msgid "is less than or equal to"
|
||||
msgstr "poniżej lub równe"
|
||||
|
||||
#: conf.py:44 conf.py:45
|
||||
msgid "starts with"
|
||||
msgstr "zaczyna się od"
|
||||
|
||||
#: conf.py:46 conf.py:47
|
||||
msgid "ends with"
|
||||
msgstr "kończy się na"
|
||||
|
||||
#: conf.py:48
|
||||
msgid "is in range"
|
||||
msgstr "zawiera się w zakresie"
|
||||
|
||||
#: conf.py:50 conf.py:51
|
||||
msgid "matches regex"
|
||||
msgstr "pasuje do wyrażenia regularnego"
|
||||
|
||||
#: conf.py:52 conf.py:60
|
||||
msgid "search"
|
||||
msgstr "szukaj"
|
||||
|
||||
#: conf.py:55
|
||||
msgid "is contained by"
|
||||
msgstr "zawiera się w"
|
||||
|
||||
#: conf.py:56
|
||||
msgid "overlaps"
|
||||
msgstr ""
|
||||
|
||||
#: conf.py:57
|
||||
msgid "has key"
|
||||
msgstr ""
|
||||
|
||||
#: conf.py:58
|
||||
msgid "has keys"
|
||||
msgstr ""
|
||||
|
||||
#: conf.py:59
|
||||
msgid "has any keys"
|
||||
msgstr ""
|
||||
|
||||
#: fields.py:172
|
||||
msgid "Range query expects two values."
|
||||
msgstr ""
|
||||
|
||||
#: filters.py:452
|
||||
msgid "Any date"
|
||||
msgstr "Dowolna data"
|
||||
|
||||
#: filters.py:453
|
||||
msgid "Today"
|
||||
msgstr "Dziś"
|
||||
|
||||
#: filters.py:458
|
||||
msgid "Past 7 days"
|
||||
msgstr "Ostatnie 7 dni"
|
||||
|
||||
#: filters.py:462
|
||||
msgid "This month"
|
||||
msgstr "Ten miesiąc"
|
||||
|
||||
#: filters.py:466
|
||||
msgid "This year"
|
||||
msgstr "Ten rok"
|
||||
|
||||
#: filters.py:469
|
||||
msgid "Yesterday"
|
||||
msgstr "Wczoraj"
|
||||
|
||||
#: filters.py:535
|
||||
msgid "Multiple values may be separated by commas."
|
||||
msgstr "Wiele wartości można rozdzielić przecinkami"
|
||||
|
||||
#: filters.py:614
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (malejąco)"
|
||||
|
||||
#: filters.py:630
|
||||
msgid "Ordering"
|
||||
msgstr "Sortowanie"
|
||||
|
||||
#: rest_framework/filterset.py:34
|
||||
#: templates/django_filters/rest_framework/form.html:5
|
||||
msgid "Submit"
|
||||
msgstr ""
|
||||
|
||||
#: templates/django_filters/rest_framework/crispy_form.html:4
|
||||
#: templates/django_filters/rest_framework/form.html:2
|
||||
#, fuzzy
|
||||
#| msgid "Filter"
|
||||
msgid "Field filters"
|
||||
msgstr "Filter"
|
||||
|
||||
#: utils.py:225
|
||||
msgid "exclude"
|
||||
msgstr ""
|
||||
|
||||
#: widgets.py:66
|
||||
msgid "All"
|
||||
msgstr "Wszystko"
|
||||
|
||||
#: widgets.py:173
|
||||
msgid "Unknown"
|
||||
msgstr ""
|
||||
|
||||
#: widgets.py:174
|
||||
msgid "Yes"
|
||||
msgstr "Tak"
|
||||
|
||||
#: widgets.py:175
|
||||
msgid "No"
|
||||
msgstr "Nie"
|
||||
|
||||
#~ msgid "This is an exclusion filter"
|
||||
#~ msgstr "Jest to filtr wykluczający"
|
Binary file not shown.
|
@ -0,0 +1,48 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: django-filter\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2013-08-10 05:34-0500\n"
|
||||
"PO-Revision-Date: 2016-09-29 11:47+0300\n"
|
||||
"Last-Translator: Mikhail Mitrofanov <mm@elec.ru>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"X-Generator: Poedit 1.8.9\n"
|
||||
|
||||
#: filters.py:153
|
||||
msgid "Any date"
|
||||
msgstr "Любая дата"
|
||||
|
||||
#: filters.py:154
|
||||
msgid "Today"
|
||||
msgstr "Сегодня"
|
||||
|
||||
#: filters.py:159
|
||||
msgid "Past 7 days"
|
||||
msgstr "Прошедшие 7 дней"
|
||||
|
||||
#: filters.py:163
|
||||
msgid "This month"
|
||||
msgstr "За этот месяц"
|
||||
|
||||
#: filters.py:167
|
||||
msgid "This year"
|
||||
msgstr "В этом году"
|
||||
|
||||
#: filterset.py:332 filterset.py:341
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s (по убыванию)"
|
||||
|
||||
#: widgets.py:63
|
||||
msgid "All"
|
||||
msgstr "Все"
|
|
@ -0,0 +1,64 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# Kane Blueriver <kxxoling@gmail.com>, 2016.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2016-01-30 17:39+0800\n"
|
||||
"PO-Revision-Date: 2016-01-30 17:50+0800\n"
|
||||
"Last-Translator: Kane Blueriver <kxxoling@gmail.com>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: filters.py:62
|
||||
msgid "This is an exclusion filter"
|
||||
msgstr "未启用该过滤器"
|
||||
|
||||
#: filters.py:62
|
||||
msgid "Filter"
|
||||
msgstr "过滤器"
|
||||
|
||||
#: filters.py:264
|
||||
msgid "Any date"
|
||||
msgstr "任何时刻"
|
||||
|
||||
#: filters.py:265
|
||||
msgid "Today"
|
||||
msgstr "今日"
|
||||
|
||||
#: filters.py:270
|
||||
msgid "Past 7 days"
|
||||
msgstr "过去 7 日"
|
||||
|
||||
#: filters.py:274
|
||||
msgid "This month"
|
||||
msgstr "本月"
|
||||
|
||||
#: filters.py:278
|
||||
msgid "This year"
|
||||
msgstr "今年"
|
||||
|
||||
#: filters.py:281
|
||||
msgid "Yesterday"
|
||||
msgstr "昨日"
|
||||
|
||||
#: filterset.py:398 filterset.py:409
|
||||
#, python-format
|
||||
msgid "%s (descending)"
|
||||
msgstr "%s(降序)"
|
||||
|
||||
#: filterset.py:411
|
||||
msgid "Ordering"
|
||||
msgstr "排序"
|
||||
|
||||
#: widgets.py:60
|
||||
msgid "All"
|
||||
msgstr "全部"
|
|
@ -0,0 +1,5 @@
|
|||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
from .backends import DjangoFilterBackend
|
||||
from .filterset import FilterSet
|
||||
from .filters import *
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import warnings
|
||||
|
||||
from django.template import loader
|
||||
from django.utils import six
|
||||
|
||||
from . import filters, filterset
|
||||
from .. import compat
|
||||
|
||||
|
||||
class DjangoFilterBackend(object):
|
||||
default_filter_set = filterset.FilterSet
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
if compat.is_crispy():
|
||||
return 'django_filters/rest_framework/crispy_form.html'
|
||||
return 'django_filters/rest_framework/form.html'
|
||||
|
||||
def get_filter_class(self, view, queryset=None):
|
||||
"""
|
||||
Return the django-filters `FilterSet` used to filter the queryset.
|
||||
"""
|
||||
filter_class = getattr(view, 'filter_class', None)
|
||||
filter_fields = getattr(view, 'filter_fields', None)
|
||||
|
||||
if filter_class:
|
||||
filter_model = filter_class.Meta.model
|
||||
|
||||
assert issubclass(queryset.model, filter_model), \
|
||||
'FilterSet model %s does not match queryset model %s' % \
|
||||
(filter_model, queryset.model)
|
||||
|
||||
return filter_class
|
||||
|
||||
if filter_fields:
|
||||
MetaBase = getattr(self.default_filter_set, 'Meta', object)
|
||||
|
||||
class AutoFilterSet(self.default_filter_set):
|
||||
class Meta(MetaBase):
|
||||
model = queryset.model
|
||||
fields = filter_fields
|
||||
|
||||
return AutoFilterSet
|
||||
|
||||
return None
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
filter_class = self.get_filter_class(view, queryset)
|
||||
|
||||
if filter_class:
|
||||
return filter_class(request.query_params, queryset=queryset, request=request).qs
|
||||
|
||||
return queryset
|
||||
|
||||
def to_html(self, request, queryset, view):
|
||||
filter_class = self.get_filter_class(view, queryset)
|
||||
if not filter_class:
|
||||
return None
|
||||
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
|
||||
|
||||
template = loader.get_template(self.template)
|
||||
context = {
|
||||
'filter': filter_instance
|
||||
}
|
||||
|
||||
return template.render(context, request)
|
||||
|
||||
def get_coreschema_field(self, field):
|
||||
if isinstance(field, filters.NumberFilter):
|
||||
field_cls = compat.coreschema.Number
|
||||
else:
|
||||
field_cls = compat.coreschema.String
|
||||
return field_cls(
|
||||
description=six.text_type(field.extra.get('help_text', ''))
|
||||
)
|
||||
|
||||
def get_schema_fields(self, view):
|
||||
# This is not compatible with widgets where the query param differs from the
|
||||
# filter's attribute name. Notably, this includes `MultiWidget`, where query
|
||||
# params will be of the format `<name>_0`, `<name>_1`, etc...
|
||||
assert compat.coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
|
||||
assert compat.coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
|
||||
|
||||
filter_class = getattr(view, 'filter_class', None)
|
||||
if filter_class is None:
|
||||
try:
|
||||
filter_class = self.get_filter_class(view, view.get_queryset())
|
||||
except Exception:
|
||||
warnings.warn(
|
||||
"{} is not compatible with schema generation".format(view.__class__)
|
||||
)
|
||||
filter_class = None
|
||||
|
||||
return [] if not filter_class else [
|
||||
compat.coreapi.Field(
|
||||
name=field_name,
|
||||
required=field.extra['required'],
|
||||
location='query',
|
||||
schema=self.get_coreschema_field(field)
|
||||
) for field_name, field in filter_class.base_filters.items()
|
||||
]
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
from ..filters import *
|
||||
from ..widgets import BooleanWidget
|
||||
|
||||
|
||||
class BooleanFilter(BooleanFilter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('widget', BooleanWidget)
|
||||
|
||||
super(BooleanFilter, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django_filters import filterset
|
||||
|
||||
from .. import compat, utils
|
||||
from .filters import BooleanFilter, IsoDateTimeFilter
|
||||
|
||||
FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
|
||||
FILTER_FOR_DBFIELD_DEFAULTS.update({
|
||||
models.DateTimeField: {'filter_class': IsoDateTimeFilter},
|
||||
models.BooleanField: {'filter_class': BooleanFilter},
|
||||
})
|
||||
|
||||
|
||||
class FilterSet(filterset.FilterSet):
|
||||
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
|
||||
|
||||
@property
|
||||
def form(self):
|
||||
form = super(FilterSet, self).form
|
||||
|
||||
if compat.is_crispy():
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Layout, Submit
|
||||
|
||||
layout_components = list(form.fields.keys()) + [
|
||||
Submit('', _('Submit'), css_class='btn-default'),
|
||||
]
|
||||
helper = FormHelper()
|
||||
helper.form_method = 'GET'
|
||||
helper.template_pack = 'bootstrap3'
|
||||
helper.layout = Layout(*layout_components)
|
||||
|
||||
form.helper = helper
|
||||
|
||||
return form
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
try:
|
||||
return super(FilterSet, self).qs
|
||||
except forms.ValidationError as e:
|
||||
raise ValidationError(utils.raw_validation(e))
|
|
@ -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 %}
|
|
@ -0,0 +1,255 @@
|
|||
import warnings
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.expressions import Expression
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.fields.related import ForeignObjectRel, RelatedField
|
||||
from django.forms import ValidationError
|
||||
from django.utils import six, timezone
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .compat import make_aware, remote_field, remote_model
|
||||
from .exceptions import FieldLookupError
|
||||
|
||||
|
||||
def deprecate(msg, level_modifier=0):
|
||||
warnings.warn(
|
||||
"%s See: https://django-filter.readthedocs.io/en/develop/migration.html" % msg,
|
||||
DeprecationWarning, stacklevel=3 + level_modifier)
|
||||
|
||||
|
||||
def try_dbfield(fn, field_class):
|
||||
"""
|
||||
Try ``fn`` with the DB ``field_class`` by walking its
|
||||
MRO until a result is found.
|
||||
|
||||
ex::
|
||||
_try_dbfield(field_dict.get, models.CharField)
|
||||
|
||||
"""
|
||||
# walk the mro, as field_class could be a derived model field.
|
||||
for cls in field_class.mro():
|
||||
# skip if cls is models.Field
|
||||
if cls is models.Field:
|
||||
continue
|
||||
|
||||
data = fn(cls)
|
||||
if data:
|
||||
return data
|
||||
|
||||
|
||||
def get_all_model_fields(model):
|
||||
opts = model._meta
|
||||
|
||||
return [
|
||||
f.name for f in sorted(opts.fields + opts.many_to_many)
|
||||
if not isinstance(f, models.AutoField) and
|
||||
not (getattr(remote_field(f), 'parent_link', False))
|
||||
]
|
||||
|
||||
|
||||
def get_model_field(model, field_name):
|
||||
"""
|
||||
Get a ``model`` field, traversing relationships
|
||||
in the ``field_name``.
|
||||
|
||||
ex::
|
||||
|
||||
f = get_model_field(Book, 'author__first_name')
|
||||
|
||||
"""
|
||||
fields = get_field_parts(model, field_name)
|
||||
return fields[-1] if fields else None
|
||||
|
||||
|
||||
def get_field_parts(model, field_name):
|
||||
"""
|
||||
Get the field parts that represent the traversable relationships from the
|
||||
base ``model`` to the final field, described by ``field_name``.
|
||||
|
||||
ex::
|
||||
|
||||
>>> parts = get_field_parts(Book, 'author__first_name')
|
||||
>>> [p.verbose_name for p in parts]
|
||||
['author', 'first name']
|
||||
|
||||
"""
|
||||
parts = field_name.split(LOOKUP_SEP)
|
||||
opts = model._meta
|
||||
fields = []
|
||||
|
||||
# walk relationships
|
||||
for name in parts:
|
||||
try:
|
||||
field = opts.get_field(name)
|
||||
except FieldDoesNotExist:
|
||||
return None
|
||||
|
||||
fields.append(field)
|
||||
if isinstance(field, RelatedField):
|
||||
opts = remote_model(field)._meta
|
||||
elif isinstance(field, ForeignObjectRel):
|
||||
opts = field.related_model._meta
|
||||
|
||||
return fields
|
||||
|
||||
|
||||
def resolve_field(model_field, lookup_expr):
|
||||
"""
|
||||
Resolves a ``lookup_expr`` into its final output field, given
|
||||
the initial ``model_field``. The lookup expression should only contain
|
||||
transforms and lookups, not intermediary model field parts.
|
||||
|
||||
Note:
|
||||
This method is based on django.db.models.sql.query.Query.build_lookup
|
||||
|
||||
For more info on the lookup API:
|
||||
https://docs.djangoproject.com/en/1.9/ref/models/lookups/
|
||||
|
||||
"""
|
||||
query = model_field.model._default_manager.all().query
|
||||
lhs = Expression(model_field)
|
||||
lookups = lookup_expr.split(LOOKUP_SEP)
|
||||
|
||||
assert len(lookups) > 0
|
||||
|
||||
try:
|
||||
while lookups:
|
||||
name = lookups[0]
|
||||
args = (lhs, name)
|
||||
if django.VERSION < (2, 0):
|
||||
# rest_of_lookups was removed in Django 2.0
|
||||
args += (lookups,)
|
||||
# If there is just one part left, try first get_lookup() so
|
||||
# that if the lhs supports both transform and lookup for the
|
||||
# name, then lookup will be picked.
|
||||
if len(lookups) == 1:
|
||||
final_lookup = lhs.get_lookup(name)
|
||||
if not final_lookup:
|
||||
# We didn't find a lookup. We are going to interpret
|
||||
# the name as transform, and do an Exact lookup against
|
||||
# it.
|
||||
lhs = query.try_transform(*args)
|
||||
final_lookup = lhs.get_lookup('exact')
|
||||
return lhs.output_field, final_lookup.lookup_name
|
||||
lhs = query.try_transform(*args)
|
||||
lookups = lookups[1:]
|
||||
except FieldError as e:
|
||||
six.raise_from(FieldLookupError(model_field, lookup_expr), e)
|
||||
|
||||
|
||||
def handle_timezone(value, is_dst=None):
|
||||
if settings.USE_TZ and timezone.is_naive(value):
|
||||
return make_aware(value, timezone.get_current_timezone(), is_dst)
|
||||
elif not settings.USE_TZ and timezone.is_aware(value):
|
||||
return timezone.make_naive(value, timezone.utc)
|
||||
return value
|
||||
|
||||
|
||||
def verbose_field_name(model, field_name):
|
||||
"""
|
||||
Get the verbose name for a given ``field_name``. The ``field_name``
|
||||
will be traversed across relationships. Returns '[invalid name]' for
|
||||
any field name that cannot be traversed.
|
||||
|
||||
ex::
|
||||
|
||||
>>> verbose_field_name(Article, 'author__name')
|
||||
'author name'
|
||||
|
||||
"""
|
||||
if field_name is None:
|
||||
return '[invalid name]'
|
||||
|
||||
parts = get_field_parts(model, field_name)
|
||||
if not parts:
|
||||
return '[invalid name]'
|
||||
|
||||
names = []
|
||||
for part in parts:
|
||||
if isinstance(part, ForeignObjectRel):
|
||||
if part.related_name:
|
||||
names.append(part.related_name.replace('_', ' '))
|
||||
else:
|
||||
return '[invalid name]'
|
||||
else:
|
||||
names.append(force_text(part.verbose_name))
|
||||
|
||||
return ' '.join(names)
|
||||
|
||||
|
||||
def verbose_lookup_expr(lookup_expr):
|
||||
"""
|
||||
Get a verbose, more humanized expression for a given ``lookup_expr``.
|
||||
Each part in the expression is looked up in the ``FILTERS_VERBOSE_LOOKUPS``
|
||||
dictionary. Missing keys will simply default to itself.
|
||||
|
||||
ex::
|
||||
|
||||
>>> verbose_lookup_expr('year__lt')
|
||||
'year is less than'
|
||||
|
||||
# with `FILTERS_VERBOSE_LOOKUPS = {}`
|
||||
>>> verbose_lookup_expr('year__lt')
|
||||
'year lt'
|
||||
|
||||
"""
|
||||
from .conf import settings as app_settings
|
||||
|
||||
VERBOSE_LOOKUPS = app_settings.VERBOSE_LOOKUPS or {}
|
||||
lookups = [
|
||||
force_text(VERBOSE_LOOKUPS.get(lookup, _(lookup)))
|
||||
for lookup in lookup_expr.split(LOOKUP_SEP)
|
||||
]
|
||||
|
||||
return ' '.join(lookups)
|
||||
|
||||
|
||||
def label_for_filter(model, field_name, lookup_expr, exclude=False):
|
||||
"""
|
||||
Create a generic label suitable for a filter.
|
||||
|
||||
ex::
|
||||
|
||||
>>> label_for_filter(Article, 'author__name', 'in')
|
||||
'auther name is in'
|
||||
|
||||
"""
|
||||
name = verbose_field_name(model, field_name)
|
||||
verbose_expression = [_('exclude'), name] if exclude else [name]
|
||||
|
||||
# iterable lookups indicate a LookupTypeField, which should not be verbose
|
||||
if isinstance(lookup_expr, six.string_types):
|
||||
verbose_expression += [verbose_lookup_expr(lookup_expr)]
|
||||
|
||||
verbose_expression = [force_text(part) for part in verbose_expression if part]
|
||||
verbose_expression = capfirst(' '.join(verbose_expression))
|
||||
|
||||
return verbose_expression
|
||||
|
||||
|
||||
def raw_validation(error):
|
||||
"""
|
||||
Deconstruct a django.forms.ValidationError into a primitive structure
|
||||
eg, plain dicts and lists.
|
||||
"""
|
||||
if isinstance(error, ValidationError):
|
||||
if hasattr(error, 'error_dict'):
|
||||
error = error.error_dict
|
||||
elif not hasattr(error, 'message'):
|
||||
error = error.error_list
|
||||
else:
|
||||
error = error.message
|
||||
|
||||
if isinstance(error, dict):
|
||||
return {key: raw_validation(value) for key, value in error.items()}
|
||||
elif isinstance(error, list):
|
||||
return [raw_validation(value) for value in error]
|
||||
else:
|
||||
return error
|
|
@ -0,0 +1,101 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.views.generic import View
|
||||
from django.views.generic.list import (
|
||||
MultipleObjectMixin,
|
||||
MultipleObjectTemplateResponseMixin
|
||||
)
|
||||
|
||||
from .constants import ALL_FIELDS
|
||||
from .filterset import filterset_factory
|
||||
|
||||
|
||||
class FilterMixin(object):
|
||||
"""
|
||||
A mixin that provides a way to show and handle a FilterSet in a request.
|
||||
"""
|
||||
filterset_class = None
|
||||
filter_fields = ALL_FIELDS
|
||||
|
||||
def get_filterset_class(self):
|
||||
"""
|
||||
Returns the filterset class to use in this view
|
||||
"""
|
||||
if self.filterset_class:
|
||||
return self.filterset_class
|
||||
elif self.model:
|
||||
return filterset_factory(model=self.model, fields=self.filter_fields)
|
||||
else:
|
||||
msg = "'%s' must define 'filterset_class' or 'model'"
|
||||
raise ImproperlyConfigured(msg % self.__class__.__name__)
|
||||
|
||||
def get_filterset(self, filterset_class):
|
||||
"""
|
||||
Returns an instance of the filterset to be used in this view.
|
||||
"""
|
||||
kwargs = self.get_filterset_kwargs(filterset_class)
|
||||
return filterset_class(**kwargs)
|
||||
|
||||
def get_filterset_kwargs(self, filterset_class):
|
||||
"""
|
||||
Returns the keyword arguments for instanciating the filterset.
|
||||
"""
|
||||
kwargs = {
|
||||
'data': self.request.GET or None,
|
||||
'request': self.request,
|
||||
}
|
||||
try:
|
||||
kwargs.update({
|
||||
'queryset': self.get_queryset(),
|
||||
})
|
||||
except ImproperlyConfigured:
|
||||
# ignore the error here if the filterset has a model defined
|
||||
# to acquire a queryset from
|
||||
if filterset_class._meta.model is None:
|
||||
msg = ("'%s' does not define a 'model' and the view '%s' does "
|
||||
"not return a valid queryset from 'get_queryset'. You "
|
||||
"must fix one of them.")
|
||||
args = (filterset_class.__name__, self.__class__.__name__)
|
||||
raise ImproperlyConfigured(msg % args)
|
||||
return kwargs
|
||||
|
||||
|
||||
class BaseFilterView(FilterMixin, MultipleObjectMixin, View):
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
filterset_class = self.get_filterset_class()
|
||||
self.filterset = self.get_filterset(filterset_class)
|
||||
self.object_list = self.filterset.qs
|
||||
context = self.get_context_data(filter=self.filterset,
|
||||
object_list=self.object_list)
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class FilterView(MultipleObjectTemplateResponseMixin, BaseFilterView):
|
||||
"""
|
||||
Render some list of objects with filter, set by `self.model` or
|
||||
`self.queryset`.
|
||||
`self.queryset` can actually be any iterable of items, not just a queryset.
|
||||
"""
|
||||
template_name_suffix = '_filter'
|
||||
|
||||
|
||||
def object_filter(request, model=None, queryset=None, template_name=None,
|
||||
extra_context=None, context_processors=None,
|
||||
filter_class=None):
|
||||
class ECFilterView(FilterView):
|
||||
"""Handle the extra_context from the functional object_filter view"""
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ECFilterView, self).get_context_data(**kwargs)
|
||||
extra_context = self.kwargs.get('extra_context') or {}
|
||||
for k, v in extra_context.items():
|
||||
if callable(v):
|
||||
v = v()
|
||||
context[k] = v
|
||||
return context
|
||||
|
||||
kwargs = dict(model=model, queryset=queryset, template_name=template_name,
|
||||
filterset_class=filter_class)
|
||||
view = ECFilterView.as_view(**kwargs)
|
||||
return view(request, extra_context=extra_context)
|
|
@ -0,0 +1,269 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from collections import Iterable
|
||||
from itertools import chain
|
||||
from re import search, sub
|
||||
|
||||
import django
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.six import string_types
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from .compat import format_value
|
||||
|
||||
|
||||
class LinkWidget(forms.Widget):
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
super(LinkWidget, self).__init__(attrs)
|
||||
|
||||
self.choices = choices
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = super(LinkWidget, self).value_from_datadict(data, files, name)
|
||||
self.data = data
|
||||
return value
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
if not hasattr(self, 'data'):
|
||||
self.data = {}
|
||||
if value is None:
|
||||
value = ''
|
||||
if django.VERSION < (1, 11):
|
||||
final_attrs = self.build_attrs(attrs)
|
||||
else:
|
||||
final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
|
||||
output = ['<ul%s>' % flatatt(final_attrs)]
|
||||
options = self.render_options(choices, [value], name)
|
||||
if options:
|
||||
output.append(options)
|
||||
output.append('</ul>')
|
||||
return mark_safe('\n'.join(output))
|
||||
|
||||
def render_options(self, choices, selected_choices, name):
|
||||
selected_choices = set(force_text(v) for v in selected_choices)
|
||||
output = []
|
||||
for option_value, option_label in chain(self.choices, choices):
|
||||
if isinstance(option_label, (list, tuple)):
|
||||
for option in option_label:
|
||||
output.append(
|
||||
self.render_option(name, selected_choices, *option))
|
||||
else:
|
||||
output.append(
|
||||
self.render_option(name, selected_choices,
|
||||
option_value, option_label))
|
||||
return '\n'.join(output)
|
||||
|
||||
def render_option(self, name, selected_choices,
|
||||
option_value, option_label):
|
||||
option_value = force_text(option_value)
|
||||
if option_label == BLANK_CHOICE_DASH[0][1]:
|
||||
option_label = _("All")
|
||||
data = self.data.copy()
|
||||
data[name] = option_value
|
||||
selected = data == self.data or option_value in selected_choices
|
||||
try:
|
||||
url = data.urlencode()
|
||||
except AttributeError:
|
||||
url = urlencode(data)
|
||||
return self.option_string() % {
|
||||
'attrs': selected and ' class="selected"' or '',
|
||||
'query_string': url,
|
||||
'label': force_text(option_label)
|
||||
}
|
||||
|
||||
def option_string(self):
|
||||
return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
|
||||
|
||||
|
||||
class SuffixedMultiWidget(forms.MultiWidget):
|
||||
"""
|
||||
A MultiWidget that allows users to provide custom suffixes instead of indexes.
|
||||
|
||||
- Suffixes must be unique.
|
||||
- There must be the same number of suffixes as fields.
|
||||
"""
|
||||
suffixes = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SuffixedMultiWidget, self).__init__(*args, **kwargs)
|
||||
|
||||
assert len(self.widgets) == len(self.suffixes)
|
||||
assert len(self.suffixes) == len(set(self.suffixes))
|
||||
|
||||
def suffixed(self, name, suffix):
|
||||
return '_'.join([name, suffix]) if suffix else name
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super(SuffixedMultiWidget, self).get_context(name, value, attrs)
|
||||
for subcontext, suffix in zip(context['widget']['subwidgets'], self.suffixes):
|
||||
subcontext['name'] = self.suffixed(name, suffix)
|
||||
|
||||
return context
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return [
|
||||
widget.value_from_datadict(data, files, self.suffixed(name, suffix))
|
||||
for widget, suffix in zip(self.widgets, self.suffixes)
|
||||
]
|
||||
|
||||
def value_omitted_from_data(self, data, files, name):
|
||||
return all(
|
||||
widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
|
||||
for widget, suffix in zip(self.widgets, self.suffixes)
|
||||
)
|
||||
|
||||
# Django < 1.11 compat
|
||||
def format_output(self, rendered_widgets):
|
||||
rendered_widgets = [
|
||||
self.replace_name(output, i)
|
||||
for i, output in enumerate(rendered_widgets)
|
||||
]
|
||||
return '\n'.join(rendered_widgets)
|
||||
|
||||
def replace_name(self, output, index):
|
||||
result = search(r'name="(?P<name>.*)_%d"' % index, output)
|
||||
name = result.group('name')
|
||||
name = self.suffixed(name, self.suffixes[index])
|
||||
name = 'name="%s"' % name
|
||||
|
||||
return sub(r'name=".*_%d"' % index, name, output)
|
||||
|
||||
def decompress(self, value):
|
||||
if value is None:
|
||||
return [None, None]
|
||||
return value
|
||||
|
||||
|
||||
class RangeWidget(forms.MultiWidget):
|
||||
template_name = 'django_filters/widgets/multiwidget.html'
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
widgets = (forms.TextInput, forms.TextInput)
|
||||
super(RangeWidget, self).__init__(widgets, attrs)
|
||||
|
||||
def format_output(self, rendered_widgets):
|
||||
# Method was removed in Django 1.11.
|
||||
return '-'.join(rendered_widgets)
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
return [value.start, value.stop]
|
||||
return [None, None]
|
||||
|
||||
|
||||
class LookupTypeWidget(forms.MultiWidget):
|
||||
def decompress(self, value):
|
||||
if value is None:
|
||||
return [None, None]
|
||||
return value
|
||||
|
||||
|
||||
class BooleanWidget(forms.Select):
|
||||
"""Convert true/false values into the internal Python True/False.
|
||||
This can be used for AJAX queries that pass true/false from JavaScript's
|
||||
internal types through.
|
||||
"""
|
||||
def __init__(self, attrs=None):
|
||||
choices = (('', _('Unknown')),
|
||||
('true', _('Yes')),
|
||||
('false', _('No')))
|
||||
super(BooleanWidget, self).__init__(attrs, choices)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
try:
|
||||
value = {
|
||||
True: 'true',
|
||||
False: 'false',
|
||||
'1': 'true',
|
||||
'0': 'false'
|
||||
}[value]
|
||||
except KeyError:
|
||||
value = ''
|
||||
return super(BooleanWidget, self).render(name, value, attrs)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = data.get(name, None)
|
||||
if isinstance(value, string_types):
|
||||
value = value.lower()
|
||||
|
||||
return {
|
||||
'1': True,
|
||||
'0': False,
|
||||
'true': True,
|
||||
'false': False,
|
||||
True: True,
|
||||
False: False,
|
||||
}.get(value, None)
|
||||
|
||||
|
||||
class BaseCSVWidget(forms.Widget):
|
||||
def _isiterable(self, value):
|
||||
return isinstance(value, Iterable) and not isinstance(value, string_types)
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
value = super(BaseCSVWidget, self).value_from_datadict(data, files, name)
|
||||
|
||||
if value is not None:
|
||||
if value == '': # empty value should parse as an empty list
|
||||
return []
|
||||
return value.split(',')
|
||||
return None
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if not self._isiterable(value):
|
||||
value = [value]
|
||||
|
||||
if len(value) <= 1:
|
||||
# delegate to main widget (Select, etc...) if not multiple values
|
||||
value = value[0] if value else ''
|
||||
return super(BaseCSVWidget, self).render(name, value, attrs)
|
||||
|
||||
# if we have multiple values, we need to force render as a text input
|
||||
# (otherwise, the additional values are lost)
|
||||
surrogate = forms.TextInput()
|
||||
value = [force_text(format_value(surrogate, v)) for v in value]
|
||||
value = ','.join(list(value))
|
||||
|
||||
return surrogate.render(name, value, attrs)
|
||||
|
||||
|
||||
class CSVWidget(BaseCSVWidget, forms.TextInput):
|
||||
pass
|
||||
|
||||
|
||||
class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
|
||||
"""
|
||||
Enables request query array notation that might be consumed by MultipleChoiceFilter
|
||||
|
||||
1. Values can be provided as csv string: ?foo=bar,baz
|
||||
2. Values can be provided as query array: ?foo[]=bar&foo[]=baz
|
||||
3. Values can be provided as query array: ?foo=bar&foo=baz
|
||||
|
||||
Note: Duplicate and empty values are skipped from results
|
||||
"""
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
if not isinstance(data, MultiValueDict):
|
||||
for key, value in data.items():
|
||||
# treat value as csv string: ?foo=1,2
|
||||
if isinstance(value, string_types):
|
||||
data[key] = [x.strip() for x in value.rstrip(',').split(',') if x]
|
||||
data = MultiValueDict(data)
|
||||
|
||||
values_list = data.getlist(name, data.getlist('%s[]' % name)) or []
|
||||
|
||||
# apparently its an array, so no need to process it's values as csv
|
||||
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
|
||||
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
|
||||
if len(values_list) > 0:
|
||||
ret = [x for x in values_list if x]
|
||||
else:
|
||||
ret = []
|
||||
|
||||
return list(set(ret))
|
Binary file not shown.
|
@ -0,0 +1,155 @@
|
|||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-filter.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-filter.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/django-filter"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-filter"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
livehtml:
|
||||
sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
|
@ -0,0 +1,254 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# django-filter documentation build configuration file, created by
|
||||
# sphinx-quickstart on Mon Sep 17 11:25:20 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys, os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.txt'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'django-filter'
|
||||
copyright = u'2013, Alex Gaynor and others.'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.1.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.1.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'django-filterdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'django-filter.tex', u'django-filter Documentation',
|
||||
u'Alex Gaynor and others.', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'django-filter', u'django-filter Documentation',
|
||||
[u'Alex Gaynor and others.'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'django-filter', u'django-filter Documentation',
|
||||
u'Alex Gaynor and others.', 'django-filter', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
|
||||
# see:
|
||||
# https://github.com/snide/sphinx_rtd_theme#using-this-theme-locally-then-building-on-read-the-docs
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
|
||||
# only import and set the theme if we're building docs locally
|
||||
if not on_rtd:
|
||||
import sphinx_rtd_theme
|
||||
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
|
@ -0,0 +1,90 @@
|
|||
======================
|
||||
Running the Test Suite
|
||||
======================
|
||||
|
||||
The easiest way to run the django-filter tests is to check out the source
|
||||
code and create a virtualenv where you can install the test dependencies.
|
||||
Django-filter uses a custom test runner to configure the environment, so a
|
||||
wrapper script is available to set up and run the test suite.
|
||||
|
||||
.. note::
|
||||
|
||||
The following assumes you have `virtualenv`__ and `git`__ installed.
|
||||
|
||||
__ https://virtualenv.pypa.io/en/stable/
|
||||
__ https://git-scm.com
|
||||
|
||||
Clone the repository
|
||||
--------------------
|
||||
|
||||
Get the source code using the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ git clone https://github.com/carltongibson/django-filter.git
|
||||
|
||||
Switch to the django-filter directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ cd django-filter
|
||||
|
||||
Set up the virtualenv
|
||||
---------------------
|
||||
|
||||
Create a new virtualenv to run the test suite in:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ virtualenv venv
|
||||
|
||||
Then activate the virtualenv and install the test requirements:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ source venv/bin/activate
|
||||
$ pip install -r requirements/test.txt
|
||||
|
||||
Execute the test runner
|
||||
-----------------------
|
||||
|
||||
Run the tests with the runner script:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python runtests.py
|
||||
|
||||
|
||||
Test all supported versions
|
||||
---------------------------
|
||||
|
||||
You can also use the excellent tox testing tool to run the tests against all
|
||||
supported versions of Python and Django. Install tox, and then simply run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install tox
|
||||
$ tox
|
||||
|
||||
|
||||
Housekeeping
|
||||
------------
|
||||
|
||||
The ``isort`` utility is used to maintain module imports. You can either test
|
||||
the module imports with the appropriate `tox` env, or with `isort` directly.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install tox
|
||||
$ tox -e isort
|
||||
|
||||
# or
|
||||
|
||||
$ pip install isort
|
||||
$ isort --check-only --recursive django_filters tests
|
||||
|
||||
To sort the imports, simply remove the ``--check-only`` option.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ isort --recursive django_filters tests
|
|
@ -0,0 +1,34 @@
|
|||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
Django-filter can be installed from PyPI with tools like ``pip``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ pip install django-filter
|
||||
|
||||
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Django-filter is tested against all supported versions of Python and `Django`__,
|
||||
as well as the latest version of Django REST Framework (`DRF`__).
|
||||
|
||||
__ https://www.djangoproject.com/download/
|
||||
__ http://www.django-rest-framework.org/
|
||||
|
||||
|
||||
|
||||
* **Python**: 2.7, 3.4, 3.5, 3.6
|
||||
* **Django**: 1.10, 1.11
|
||||
* **DRF**: 3.7
|
|
@ -0,0 +1,231 @@
|
|||
===============
|
||||
Migration Guide
|
||||
===============
|
||||
|
||||
----------------
|
||||
Migrating to 2.0
|
||||
----------------
|
||||
|
||||
Removal of the ``Meta.together`` option
|
||||
---------------------------------------
|
||||
|
||||
The ``Meta.together`` has been deprecated in favor of userland implementations
|
||||
that override the ``clean`` method of the ``Meta.form`` class. An example will
|
||||
be provided in a "recipes" secion in future docs.
|
||||
|
||||
``Filter.name`` renamed to ``Filter.field_name``
|
||||
------------------------------------------------
|
||||
|
||||
The filter ``name`` has been renamed to ``field_name`` as a way to disambiguate
|
||||
the filter's attribute name on its FilterSet class from the ``field_name`` used
|
||||
for filtering purposes.
|
||||
|
||||
FilterSet strictness has been removed
|
||||
-------------------------------------
|
||||
|
||||
Strictness handling has been removed from the ``FilterSet`` and added to the
|
||||
view layer. As a result, the ``FILTERS_STRICTNESS`` setting, ``Meta.strict``
|
||||
option, and ``strict`` argument for the ``FilterSet`` initializer have all
|
||||
been removed.
|
||||
|
||||
To alter strictness behavior, the appropriate view code should be overridden.
|
||||
More details will be provided in future docs.
|
||||
|
||||
|
||||
----------------
|
||||
Migrating to 1.0
|
||||
----------------
|
||||
|
||||
The 1.0 release of django-filter introduces several API changes and refinements
|
||||
that break forwards compatibility. Below is a list of deprecations and
|
||||
instructions on how to migrate to the 1.0 release. A forwards-compatible 0.15
|
||||
release has also been created to help with migration. It is compatible with
|
||||
both the existing and new APIs and will raise warnings for deprecated behavior.
|
||||
|
||||
Enabling warnings
|
||||
-----------------
|
||||
|
||||
To view the deprecations, you may need to enable warnings within python. This
|
||||
can be achieved with either the ``-W`` `flag`__, or with ``PYTHONWARNINGS``
|
||||
`environment variable`__. For example, you could run your test suite like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python -W once manage.py test
|
||||
|
||||
The above would print all warnings once when they first occur. This is useful
|
||||
to know what violations exist in your code (or occasionally in third party
|
||||
code). However, it only prints the last line of the stack trace. You can use
|
||||
the following to raise the full exception instead:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ python -W error manage.py test
|
||||
|
||||
__ https://docs.python.org/3.6/using/cmdline.html#cmdoption-W
|
||||
__ https://docs.python.org/3.6/using/cmdline.html#envvar-PYTHONWARNINGS
|
||||
|
||||
|
||||
MethodFilter and Filter.action replaced by Filter.method
|
||||
--------------------------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/pull/382
|
||||
|
||||
The functionality of ``MethodFilter`` and ``Filter.action`` has been merged
|
||||
together and replaced by the ``Filter.method`` parameter. The ``method``
|
||||
parameter takes either a callable or the name of a ``FilterSet`` method. The
|
||||
signature now takes an additional ``name`` argument that is the name of the
|
||||
model field to be filtered on.
|
||||
|
||||
Since ``method`` is now a parameter of all filters, inputs are validated and
|
||||
cleaned by its ``field_class``. The function will receive the cleaned value
|
||||
instead of the raw value.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 0.x
|
||||
class UserFilter(FilterSet):
|
||||
last_login = filters.MethodFilter()
|
||||
|
||||
def filter_last_login(self, qs, value):
|
||||
# try to convert value to datetime, which may fail.
|
||||
if value and looks_like_a_date(value):
|
||||
value = datetime(value)
|
||||
|
||||
return qs.filter(last_login=value})
|
||||
|
||||
|
||||
# 1.0
|
||||
class UserFilter(FilterSet):
|
||||
last_login = filters.CharFilter(method='filter_last_login')
|
||||
|
||||
def filter_last_login(self, qs, name, value):
|
||||
return qs.filter(**{name: value})
|
||||
|
||||
|
||||
QuerySet methods are no longer proxied
|
||||
--------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/pull/440
|
||||
|
||||
The ``__iter__()``, ``__len__()``, ``__getitem__()``, ``count()`` methods are
|
||||
no longer proxied from the queryset. To fix this, call the methods on the
|
||||
``.qs`` property itself.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
f = UserFilter(request.GET, queryset=User.objects.all())
|
||||
|
||||
# 0.x
|
||||
for obj in f:
|
||||
...
|
||||
|
||||
# 1.0
|
||||
for obj in f.qs:
|
||||
...
|
||||
|
||||
|
||||
Filters no longer autogenerated when Meta.fields is not specified
|
||||
-----------------------------------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/pull/450
|
||||
|
||||
FilterSets had an undocumented behavior of autogenerating filters for all
|
||||
model fields when either ``Meta.fields`` was not specified or when set to
|
||||
``None``. This can lead to potentially unsafe data or schema exposure and
|
||||
has been deprecated in favor of explicitly setting ``Meta.fields`` to the
|
||||
``'__all__'`` special value. You may also blacklist fields by setting
|
||||
the ``Meta.exclude`` attribute.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
# or
|
||||
class UserFilter(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ['password']
|
||||
|
||||
|
||||
Move FilterSet options to Meta class
|
||||
------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/issues/430
|
||||
|
||||
Several ``FilterSet`` options have been moved to the ``Meta`` class to prevent
|
||||
potential conflicts with declared filter names. This includes:
|
||||
|
||||
* ``filter_overrides``
|
||||
* ``strict``
|
||||
* ``order_by_field``
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# 0.x
|
||||
class UserFilter(FilterSet):
|
||||
filter_overrides = {}
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
order_by_field = 'order'
|
||||
...
|
||||
|
||||
# 1.0
|
||||
class UserFilter(FilterSet):
|
||||
...
|
||||
|
||||
class Meta:
|
||||
filter_overrides = {}
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
order_by_field = 'order'
|
||||
|
||||
|
||||
FilterSet ordering replaced by OrderingFilter
|
||||
---------------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/pull/472
|
||||
|
||||
The FilterSet ordering options and methods have been deprecated and replaced
|
||||
by :ref:`OrderingFilter <ordering-filter>`. Deprecated options include:
|
||||
|
||||
|
||||
* ``Meta.order_by``
|
||||
* ``Meta.order_by_field``
|
||||
|
||||
These options retain backwards compatibility with the following caveats:
|
||||
|
||||
* ``order_by`` asserts that ``Meta.fields`` is not using the dict syntax. This
|
||||
previously was undefined behavior, however the migration code is unable to
|
||||
support it.
|
||||
* Prior, if no ordering was specified in the request, the FilterSet implicitly
|
||||
filtered by the first param in the ``order_by`` option. This behavior cannot
|
||||
be easily emulated but can be fixed by ensuring that the passed in queryset
|
||||
explicitly calls ``.order_by()``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
filterset = MyFilterSet(queryset=MyModel.objects.order_by('field'))
|
||||
|
||||
The following methods are deprecated and will raise an assertion if present
|
||||
on the FilterSet:
|
||||
|
||||
* ``.get_order_by()``
|
||||
* ``.get_ordering_field()``
|
||||
|
||||
To fix this, simply remove the methods from your class. You can subclass
|
||||
``OrderingFilter`` to migrate any custom logic.
|
||||
|
||||
|
||||
Deprecated ``FILTERS_HELP_TEXT_FILTER`` and ``FILTERS_HELP_TEXT_EXCLUDE``
|
||||
-------------------------------------------------------------------------
|
||||
Details: https://github.com/carltongibson/django-filter/pull/437
|
||||
|
||||
Generated filter labels in 1.0 will be more descriptive, including humanized
|
||||
text about the lookup being performed and if the filter is an exclusion filter.
|
||||
|
||||
These settings will no longer have an effect and will be removed in the 1.0 release.
|
||||
|
||||
|
||||
DRF filter backend raises ``TemplateDoesNotExist`` exception
|
||||
------------------------------------------------------------
|
||||
|
||||
Templates are now provided by django-filter. If you are receiving this error,
|
||||
you may need to add ``'django_filters'`` to your ``INSTALLED_APPS`` setting.
|
||||
Alternatively, you could provide your own templates.
|
|
@ -0,0 +1,189 @@
|
|||
====================
|
||||
Integration with DRF
|
||||
====================
|
||||
|
||||
Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package.
|
||||
|
||||
__ http://www.django-rest-framework.org/
|
||||
__ http://www.django-rest-framework.org/api-guide/filtering/
|
||||
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
|
||||
Using the new ``FilterSet`` simply requires changing the import path. Instead of importing from ``django_filters``, import from the ``rest_framework`` sub-package.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
class ProductFilter(filters.FilterSet):
|
||||
...
|
||||
|
||||
Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_backends``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django_filters import rest_framework as filters
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filter_fields = ('category', 'in_stock')
|
||||
|
||||
If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_FILTER_BACKENDS': (
|
||||
'django_filters.rest_framework.DjangoFilterBackend',
|
||||
...
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
Adding a FilterSet with ``filter_class``
|
||||
----------------------------------------
|
||||
|
||||
To enable filtering with a ``FilterSet``, add it to the ``filter_class`` parameter on your view class.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from rest_framework import generics
|
||||
from django_filters import rest_framework as filters
|
||||
from myapp import Product
|
||||
|
||||
|
||||
class ProductFilter(filters.FilterSet):
|
||||
min_price = filters.NumberFilter(name="price", lookup_expr='gte')
|
||||
max_price = filters.NumberFilter(name="price", lookup_expr='lte')
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['category', 'in_stock', 'min_price', 'max_price']
|
||||
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filter_class = ProductFilter
|
||||
|
||||
|
||||
Using the ``filter_fields`` shortcut
|
||||
------------------------------------
|
||||
|
||||
You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields <fields>`.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from rest_framework import generics
|
||||
from django_filters import rest_framework as filters
|
||||
from myapp import Product
|
||||
|
||||
|
||||
class ProductList(generics.ListAPIView):
|
||||
queryset = Product.objects.all()
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filter_fields = ('category', 'in_stock')
|
||||
|
||||
|
||||
# Equivalent FilterSet:
|
||||
class ProductFilter(filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ('category', 'in_stock')
|
||||
|
||||
|
||||
Schema Generation with Core API
|
||||
-------------------------------
|
||||
|
||||
The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()``. This is automatically enabled when Core API is installed. Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``.
|
||||
|
||||
For example, your get queryset method may look like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_project(self):
|
||||
return models.Project.objects.get(pk=self.kwargs['project_id'])
|
||||
|
||||
def get_queryset(self):
|
||||
project = self.get_project()
|
||||
|
||||
return self.queryset \
|
||||
.filter(project=project) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
This could be rewritten like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_project(self):
|
||||
try:
|
||||
return models.Project.objects.get(pk=self.kwargs['project_id'])
|
||||
except models.Project.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_queryset(self):
|
||||
project = self.get_project()
|
||||
|
||||
if project is None:
|
||||
return self.queryset.none()
|
||||
|
||||
return self.queryset \
|
||||
.filter(project=project) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
Or more simply as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class IssueViewSet(views.ModelViewSet):
|
||||
queryset = models.Issue.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
# project_id may be None
|
||||
return self.queryset \
|
||||
.filter(project_id=self.kwargs.get('project_id')) \
|
||||
.filter(author=self.request.user)
|
||||
|
||||
|
||||
Crispy Forms
|
||||
------------
|
||||
|
||||
If you are using DRF's browsable API or admin API you may also want to install ``django-crispy-forms``, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. Note that this isn't actively supported, although pull requests for bug fixes are welcome.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install django-crispy-forms
|
||||
|
||||
With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so:
|
||||
|
||||
.. image:: ../assets/form.png
|
||||
|
||||
|
||||
Additional ``FilterSet`` Features
|
||||
---------------------------------
|
||||
|
||||
The following features are specific to the rest framework FilterSet:
|
||||
|
||||
- ``BooleanFilter``'s use the API-friendly ``BooleanWidget``, which accepts lowercase ``true``/``false``.
|
||||
- Filter generation uses ``IsoDateTimeFilter`` for datetime model fields.
|
||||
- Raised ``ValidationError``'s are reraised as their DRF equivalent. This behavior is useful when setting FilterSet
|
||||
strictness to ``STRICTNESS.RAISE_VALIDATION_ERROR``.
|
|
@ -0,0 +1,246 @@
|
|||
==================
|
||||
Tips and Solutions
|
||||
==================
|
||||
|
||||
Common problems for declared filters
|
||||
------------------------------------
|
||||
|
||||
Below are some of the common problem that occur when declaring filters. It is
|
||||
recommended that you read this as it provides a more complete understanding of
|
||||
how filters work.
|
||||
|
||||
|
||||
Filter ``name`` and ``lookup_expr`` not configured
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
|
||||
them. By default, if ``name`` is not specified, the filter's name on the
|
||||
filterset class will be used. Additionally, ``lookup_expr`` defaults to
|
||||
``exact``. The following is an example of a misconfigured price filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter()
|
||||
|
||||
The filter instance will have a field name of ``price__gt`` and an ``exact``
|
||||
lookup type. Under the hood, this will incorrectly be resolved as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
Produce.objects.filter(price__gt__exact=value)
|
||||
|
||||
The above will most likely generate a ``FieldError``. The correct configuration
|
||||
would be:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
|
||||
|
||||
|
||||
Missing ``lookup_expr`` for text search filters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's quite common to forget to set the lookup expression for :code:`CharField`
|
||||
and :code:`TextField` and wonder why a search for "foo" does not return results
|
||||
for "foobar". This is because the default lookup type is ``exact``, but you
|
||||
probably want to perform an ``icontains`` lookup.
|
||||
|
||||
|
||||
Filter and lookup expression mismatch (in, range, isnull)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's not always appropriate to directly match a filter to its model field's
|
||||
type, as some lookups expect different types of values. This is a commonly
|
||||
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
|
||||
at the following product model:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Product(models.Model):
|
||||
category = models.ForeignKey(Category, null=True)
|
||||
|
||||
Given that ``category`` is optional, it's reasonable to want to enable a search
|
||||
for uncategorized products. The following is an incorrectly configured
|
||||
``isnull`` filter:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
So what's the issue? While the underlying column type for ``category`` is an
|
||||
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
|
||||
only validates numbers. Filters are not `'expression aware'` and won't change
|
||||
behavior based on their ``lookup_expr``. You should use filters that match the
|
||||
data type of the lookup expression `instead` of the data type underlying the
|
||||
model field. The following would correctly allow you to search for both
|
||||
uncategorized products and products for a set of categories:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
categories = NumberInFilter(name='category', lookup_expr='in')
|
||||
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
More info on constructing ``in`` and ``range`` csv :ref:`filters <base-in-filter>`.
|
||||
|
||||
|
||||
Filtering by empty values
|
||||
-------------------------
|
||||
|
||||
There are a number of cases where you may need to filter by empty or null
|
||||
values. The following are some common solutions to these problems:
|
||||
|
||||
|
||||
Filtering by null values
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As explained in the above "Filter and lookup expression mismatch" section, a
|
||||
common problem is how to correctly filter by null values on a field.
|
||||
|
||||
Solution 1: Using a ``BooleanFilter`` with ``isnull``
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by
|
||||
the FilterSet's automatic filter generation. To do this manually, simply add:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that the filter class is validating the input value. The underlying
|
||||
type of the mode field is not relevant here.
|
||||
|
||||
You may also reverse the logic with the ``exclude`` parameter.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True)
|
||||
|
||||
Solution 2: Using ``ChoiceFilter``'s null choice
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
If you're using a ChoiceFilter, you may also filter by null values by enabling
|
||||
the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
|
||||
:ref:`docs <choice-filter>`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
category = django_filters.ModelChoiceFilter(
|
||||
name='category', lookup_expr='isnull',
|
||||
null_label='Uncategorized',
|
||||
queryset=Category.objects.all(),
|
||||
)
|
||||
|
||||
Solution 3: Combining fields w/ ``MultiValueField``
|
||||
"""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
An alternative approach is to use Django's ``MultiValueField`` to manually add
|
||||
in a ``BooleanField`` to handle null values. Proof of concept:
|
||||
https://github.com/carltongibson/django-filter/issues/446
|
||||
|
||||
|
||||
Filtering by an empty string
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's not currently possible to filter by an empty string, since empty values are
|
||||
interpreted as a skipped filter.
|
||||
|
||||
GET http://localhost/api/my-model?myfield=
|
||||
|
||||
Solution 1: Magic values
|
||||
""""""""""""""""""""""""
|
||||
|
||||
You can override the ``filter()`` method of a filter class to specifically check
|
||||
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
|
||||
|
||||
GET http://localhost/api/my-model?myfield=EMPTY
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyCharFilter(filters.CharFilter):
|
||||
empty_value = 'EMPTY'
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value != self.empty_value:
|
||||
return super(MyCharFilter, self).filter(qs, value)
|
||||
|
||||
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""})
|
||||
return qs.distinct() if self.distinct else qs
|
||||
|
||||
|
||||
Solution 2: Empty string filter
|
||||
"""""""""""""""""""""""""""""""
|
||||
|
||||
It would also be possible to create an empty value filter that exhibits the same
|
||||
behavior as an ``isnull`` filter.
|
||||
|
||||
GET http://localhost/api/my-model?myfield__isempty=false
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
|
||||
class EmptyStringFilter(filters.BooleanFilter):
|
||||
def filter(self, qs, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return qs
|
||||
|
||||
exclude = self.exclude ^ (value is False)
|
||||
method = qs.exclude if exclude else qs.filter
|
||||
|
||||
return method(**{self.name: ""})
|
||||
|
||||
|
||||
class MyFilterSet(filters.FilterSet):
|
||||
myfield__isempty = EmptyStringFilter(name='myfield')
|
||||
|
||||
class Meta:
|
||||
model = MyModel
|
||||
|
||||
|
||||
Using ``initial`` values as defaults
|
||||
------------------------------------
|
||||
|
||||
In pre-1.0 versions of django-filter, a filter field's ``initial`` value was used as a
|
||||
default when no value was submitted. This behavior was not officially supported and has
|
||||
since been removed.
|
||||
|
||||
|
||||
.. warning:: It is recommended that you do **NOT** implement the below as it adversely
|
||||
affects usability. Django forms don't provide this behavior for a reason.
|
||||
|
||||
- Using initial values as defaults is inconsistent with the behavior of Django forms.
|
||||
- Default values prevent users from filtering by empty values.
|
||||
- Default values prevent users from skipping that filter.
|
||||
|
||||
If defaults are necessary though, the following should mimic the pre-1.0 behavior:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class BaseFilterSet(FilterSet):
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
# if filterset is bound, use initial values as defaults
|
||||
if data is not None:
|
||||
# get a mutable copy of the QueryDict
|
||||
data = data.copy()
|
||||
|
||||
for name, f in self.base_filters.items():
|
||||
initial = f.extra.get('initial')
|
||||
|
||||
# filter param is either missing or empty, use initial as default
|
||||
if not data.get(name) and initial:
|
||||
data[name] = initial
|
||||
|
||||
super(BaseFilterSet, self).__init__(data, *args, **kwargs)
|
|
@ -0,0 +1,337 @@
|
|||
===============
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Django-filter provides a simple way to filter down a queryset based on
|
||||
parameters a user provides. Say we have a ``Product`` model and we want to let
|
||||
our users filter which products they see on a list page.
|
||||
|
||||
.. note::
|
||||
|
||||
If you're using django-filter with Django Rest Framework, it's
|
||||
recommended that you read the integration docs after this guide.
|
||||
|
||||
The model
|
||||
---------
|
||||
|
||||
Let's start with our model::
|
||||
|
||||
from django.db import models
|
||||
|
||||
class Product(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
price = models.DecimalField()
|
||||
description = models.TextField()
|
||||
release_date = models.DateField()
|
||||
manufacturer = models.ForeignKey(Manufacturer)
|
||||
|
||||
The filter
|
||||
----------
|
||||
|
||||
We have a number of fields and we want to let our users filter based on the
|
||||
name, the price or the release_date. We create a ``FilterSet`` for this::
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr='iexact')
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['price', 'release_date']
|
||||
|
||||
|
||||
As you can see this uses a very similar API to Django's ``ModelForm``. Just
|
||||
like with a ``ModelForm`` we can also override filters, or add new ones using a
|
||||
declarative syntax.
|
||||
|
||||
Declaring filters
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The declarative syntax provides you with the most flexibility when creating
|
||||
filters, however it is fairly verbose. We'll use the below example to outline
|
||||
the :ref:`core filter arguments <core-arguments>` on a ``FilterSet``::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
price = django_filters.NumberFilter()
|
||||
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
|
||||
price__lt = django_filters.NumberFilter(name='price', lookup_expr='lt')
|
||||
|
||||
release_year = django_filters.NumberFilter(name='release_date', lookup_expr='year')
|
||||
release_year__gt = django_filters.NumberFilter(name='release_date', lookup_expr='year__gt')
|
||||
release_year__lt = django_filters.NumberFilter(name='release_date', lookup_expr='year__lt')
|
||||
|
||||
manufacturer__name = django_filters.CharFilter(lookup_expr='icontains')
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
|
||||
There are two main arguments for filters:
|
||||
|
||||
- ``name``: The name of the model field to filter on. You can traverse
|
||||
"relationship paths" using Django's ``__`` syntax to filter fields on a
|
||||
related model. ex, ``manufacturer__name``.
|
||||
- ``lookup_expr``: The `field lookup`_ to use when filtering. Django's ``__``
|
||||
syntax can again be used in order to support lookup transforms.
|
||||
ex, ``year__gte``.
|
||||
|
||||
.. _`field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
||||
|
||||
Together, the field ``name`` and ``lookup_expr`` represent a complete Django
|
||||
lookup expression. A detailed explanation of lookup expressions is provided in
|
||||
Django's `lookup reference`_. django-filter supports expressions containing
|
||||
both transforms and a final lookup for version 1.9 of Django and above.
|
||||
For Django version 1.8, transformed expressions are not supported.
|
||||
|
||||
.. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups
|
||||
|
||||
|
||||
Generating filters with Meta.fields
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The FilterSet Meta class provides a ``fields`` attribute that can be used for
|
||||
easily specifying multiple filters without significant code duplication. The
|
||||
base syntax supports a list of multiple field names::
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['price', 'release_date']
|
||||
|
||||
The above generates 'exact' lookups for both the 'price' and 'release_date'
|
||||
fields.
|
||||
|
||||
Additionally, a dictionary can be used to specify multiple lookup expressions
|
||||
for each field::
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = {
|
||||
'price': ['lt', 'gt'],
|
||||
'release_date': ['exact', 'year__gt'],
|
||||
}
|
||||
|
||||
The above would generate 'price__lt', 'price__gt', 'release_date', and
|
||||
'release_date__year__gt' filters.
|
||||
|
||||
.. note::
|
||||
|
||||
The filter lookup type 'exact' is an implicit default and therefore never
|
||||
added to a filter name. In the above example, the release date's exact
|
||||
filter is 'release_date', not 'release_date__exact'.
|
||||
|
||||
Items in the ``fields`` sequence in the ``Meta`` class may include
|
||||
"relationship paths" using Django's ``__`` syntax to filter on fields on a
|
||||
related model::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['manufacturer__country']
|
||||
|
||||
|
||||
Overriding default filters
|
||||
""""""""""""""""""""""""""
|
||||
|
||||
Like ``django.contrib.admin.ModelAdmin``, it is possible to override
|
||||
default filters for all the models fields of the same kind using
|
||||
``filter_overrides`` on the ``Meta`` class::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = {
|
||||
'name': ['exact'],
|
||||
'release_date': ['isnull'],
|
||||
}
|
||||
filter_overrides = {
|
||||
models.CharField: {
|
||||
'filter_class': django_filters.CharFilter,
|
||||
'extra': lambda f: {
|
||||
'lookup_expr': 'icontains',
|
||||
},
|
||||
},
|
||||
models.BooleanField: {
|
||||
'filter_class': django_filters.BooleanFilter,
|
||||
'extra': lambda f: {
|
||||
'widget': forms.CheckboxInput,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Request-based filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``FilterSet`` may be initialized with an optional ``request`` argument. If
|
||||
a request object is passed, then you may access the request during filtering.
|
||||
This allows you to filter by properties on the request, such as the currently
|
||||
logged-in user or the ``Accepts-Languages`` header.
|
||||
|
||||
.. note::
|
||||
|
||||
It is not guaranteed that a `request` will be provied to the `FilterSet`
|
||||
instance. Any code depending on a request should handle the `None` case.
|
||||
|
||||
|
||||
Filtering the primary ``.qs``
|
||||
"""""""""""""""""""""""""""""
|
||||
|
||||
To filter the primary queryset by the ``request`` object, simply override the
|
||||
``FilterSet.qs`` property. For example, you could filter blog articles to only
|
||||
those that are published and those that are owned by the logged-in user
|
||||
(presumably the author's draft articles).
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ArticleFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = [...]
|
||||
|
||||
@property
|
||||
def qs(self):
|
||||
parent = super(ArticleFilter, self).qs
|
||||
author = getattr(self.request, 'user', None)
|
||||
|
||||
return parent.filter(is_published=True) \
|
||||
| parent.filter(author=author)
|
||||
|
||||
|
||||
Filtering the related queryset for ``ModelChoiceFilter``
|
||||
""""""""""""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The ``queryset`` argument for ``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter``
|
||||
supports callable behavior. If a callable is passed, it will be invoked with the
|
||||
``request`` as its only argument. This allows you to perform the same kinds of
|
||||
request-based filtering without resorting to overriding ``FilterSet.__init__``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def departments(request):
|
||||
if request is None:
|
||||
return Department.objects.none()
|
||||
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
|
||||
Customize filtering with ``Filter.method``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can control the behavior of a filter by specifying a ``method`` to perform
|
||||
filtering. View more information in the :ref:`method reference <filter-method>`.
|
||||
Note that you may access the filterset's properties, such as the ``request``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class F(django_filters.FilterSet):
|
||||
username = CharFilter(method='my_custom_filter')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
def my_custom_filter(self, queryset, name, value):
|
||||
return queryset.filter(**{
|
||||
name: value,
|
||||
})
|
||||
|
||||
|
||||
The view
|
||||
--------
|
||||
|
||||
Now we need to write a view::
|
||||
|
||||
def product_list(request):
|
||||
f = ProductFilter(request.GET, queryset=Product.objects.all())
|
||||
return render(request, 'my_app/template.html', {'filter': f})
|
||||
|
||||
If a queryset argument isn't provided then all the items in the default manager
|
||||
of the model will be used.
|
||||
|
||||
If you want to access the filtered objects in your views, for example if you
|
||||
want to paginate them, you can do that. They are in f.qs
|
||||
|
||||
|
||||
The URL conf
|
||||
------------
|
||||
|
||||
We need a URL pattern to call the view::
|
||||
|
||||
url(r'^list$', views.product_list)
|
||||
|
||||
|
||||
The template
|
||||
------------
|
||||
|
||||
And lastly we need a template::
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="get">
|
||||
{{ filter.form.as_p }}
|
||||
<input type="submit" />
|
||||
</form>
|
||||
{% for obj in filter.qs %}
|
||||
{{ obj.name }} - ${{ obj.price }}<br />
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
And that's all there is to it! The ``form`` attribute contains a normal
|
||||
Django form, and when we iterate over the ``FilterSet.qs`` we get the objects in
|
||||
the resulting queryset.
|
||||
|
||||
|
||||
Generic view & configuration
|
||||
-----------------------------
|
||||
|
||||
In addition to the above usage there is also a class-based generic view
|
||||
included in django-filter, which lives at ``django_filters.views.FilterView``.
|
||||
You must provide either a ``model`` or ``filterset_class`` argument, similar to
|
||||
``ListView`` in Django itself::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django_filters.views import FilterView
|
||||
from myapp.models import Product
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^list/$', FilterView.as_view(model=Product)),
|
||||
]
|
||||
|
||||
If you provide a ``model`` optionally you can set ``filter_fields`` to specify a list or a tuple of
|
||||
the fields that you want to include for the automatic construction of the filterset class.
|
||||
|
||||
You must provide a template at ``<app>/<model>_filter.html`` which gets the
|
||||
context parameter ``filter``. Additionally, the context will contain
|
||||
``object_list`` which holds the filtered queryset.
|
||||
|
||||
A legacy functional generic view is still included in django-filter, although
|
||||
its use is deprecated. It can be found at
|
||||
``django_filters.views.object_filter``. You must provide the same arguments
|
||||
to it as the class based view::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django_filters.views import object_filter
|
||||
from myapp.models import Product
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^list/$', object_filter, {'model': Product}),
|
||||
]
|
||||
|
||||
The needed template and its context variables will also be the same as the
|
||||
class-based view above.
|
|
@ -0,0 +1,34 @@
|
|||
=============
|
||||
django-filter
|
||||
=============
|
||||
|
||||
Django-filter is a generic, reusable application to alleviate writing some of
|
||||
the more mundane bits of view code. Specifically, it allows users to filter
|
||||
down a queryset based on a model's fields, displaying the form to let them
|
||||
do this.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: User Guide
|
||||
|
||||
guide/install
|
||||
guide/usage
|
||||
guide/rest_framework
|
||||
guide/tips
|
||||
guide/migration
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Reference Documentation
|
||||
|
||||
ref/filterset
|
||||
ref/filters
|
||||
ref/fields
|
||||
ref/widgets
|
||||
ref/settings
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Developer Documentation
|
||||
|
||||
dev/tests
|
|
@ -0,0 +1,190 @@
|
|||
@ECHO OFF
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set BUILDDIR=_build
|
||||
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||
if NOT "%PAPER%" == "" (
|
||||
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||
)
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
if "%1" == "help" (
|
||||
:help
|
||||
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||
echo. html to make standalone HTML files
|
||||
echo. dirhtml to make HTML files named index.html in directories
|
||||
echo. singlehtml to make a single large HTML file
|
||||
echo. pickle to make pickle files
|
||||
echo. json to make JSON files
|
||||
echo. htmlhelp to make HTML files and a HTML help project
|
||||
echo. qthelp to make HTML files and a qthelp project
|
||||
echo. devhelp to make HTML files and a Devhelp project
|
||||
echo. epub to make an epub
|
||||
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||
echo. text to make text files
|
||||
echo. man to make manual pages
|
||||
echo. texinfo to make Texinfo files
|
||||
echo. gettext to make PO message catalogs
|
||||
echo. changes to make an overview over all changed/added/deprecated items
|
||||
echo. linkcheck to check all external links for integrity
|
||||
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "clean" (
|
||||
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||
del /q /s %BUILDDIR%\*
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "html" (
|
||||
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "dirhtml" (
|
||||
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "singlehtml" (
|
||||
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "pickle" (
|
||||
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the pickle files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "json" (
|
||||
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can process the JSON files.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "htmlhelp" (
|
||||
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "qthelp" (
|
||||
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-filter.qhcp
|
||||
echo.To view the help file:
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-filter.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "devhelp" (
|
||||
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "epub" (
|
||||
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "latex" (
|
||||
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "text" (
|
||||
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "man" (
|
||||
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "texinfo" (
|
||||
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "gettext" (
|
||||
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "changes" (
|
||||
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.The overview file is in %BUILDDIR%/changes.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "linkcheck" (
|
||||
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Link check complete; look for any errors in the above output ^
|
||||
or in %BUILDDIR%/linkcheck/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
if "%1" == "doctest" (
|
||||
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||
if errorlevel 1 exit /b 1
|
||||
echo.
|
||||
echo.Testing of doctests in the sources finished, look at the ^
|
||||
results in %BUILDDIR%/doctest/output.txt.
|
||||
goto end
|
||||
)
|
||||
|
||||
:end
|
|
@ -0,0 +1,23 @@
|
|||
===============
|
||||
Field Reference
|
||||
===============
|
||||
|
||||
``IsoDateTimeField``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extends ``django.forms.DateTimeField`` to allow parsing ISO 8601 formated dates, in addition to existing formats
|
||||
|
||||
Defines a class level attribute ``ISO_8601`` as constant for the format.
|
||||
|
||||
Sets ``input_formats = [ISO_8601]`` — this means that by default ``IsoDateTimeField`` will **only** parse ISO 8601 formated dates.
|
||||
|
||||
You may set ``input_formats`` to your list of required formats as per the `DateTimeField Docs`_, using the ``ISO_8601`` class level attribute to specify the ISO 8601 format.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
f = IsoDateTimeField()
|
||||
f.input_formats = [IsoDateTimeField.ISO_8601] + DateTimeField.input_formats
|
||||
|
||||
|
||||
.. _`DateTimeField Docs`: https://docs.djangoproject.com/en/1.8/ref/forms/fields/#django.forms.DateTimeField.input_formats
|
||||
|
|
@ -0,0 +1,788 @@
|
|||
================
|
||||
Filter Reference
|
||||
================
|
||||
|
||||
This is a reference document with a list of the filters and their arguments.
|
||||
|
||||
.. _core-arguments:
|
||||
|
||||
Core Arguments
|
||||
--------------
|
||||
|
||||
The following are the core arguments that apply to all filters.
|
||||
|
||||
``name``
|
||||
~~~~~~~~
|
||||
|
||||
The name of the field this filter is supposed to filter on, if this is not
|
||||
provided it automatically becomes the filter's name on the ``FilterSet``.
|
||||
You can traverse "relationship paths" using Django's ``__`` syntax to filter
|
||||
fields on a related model. eg, ``manufacturer__name``.
|
||||
|
||||
``label``
|
||||
~~~~~~~~~
|
||||
|
||||
The label as it will apear in the HTML, analogous to a form field's label
|
||||
argument. If a label is not provided, a verbose label will be generated based
|
||||
on the field ``name`` and the parts of the ``lookup_expr``.
|
||||
(See: :ref:`verbose-lookups-setting`).
|
||||
|
||||
``widget``
|
||||
~~~~~~~~~~
|
||||
|
||||
The django.form Widget class which will represent the ``Filter``. In addition
|
||||
to the widgets that are included with Django that you can use there are
|
||||
additional ones that django-filter provides which may be useful:
|
||||
|
||||
* :ref:`LinkWidget <link-widget>` -- this displays the options in a manner
|
||||
similar to the way the Django Admin does, as a series of links. The link
|
||||
for the selected option will have ``class="selected"``.
|
||||
* :ref:`BooleanWidget <boolean-widget>` -- this widget converts its input
|
||||
into Python's True/False values. It will convert all case variations of
|
||||
``True`` and ``False`` into the internal Python values.
|
||||
* :ref:`CSVWidget <csv-widget>` -- this widget expects a comma separated
|
||||
value and converts it into a list of string values. It is expected that
|
||||
the field class handle a list of values as well as type conversion.
|
||||
* :ref:`RangeWidget <range-widget>` -- this widget is used with ``RangeFilter``
|
||||
to generate two form input elements using a single field.
|
||||
|
||||
.. _filter-method:
|
||||
|
||||
``method``
|
||||
~~~~~~~~~~
|
||||
|
||||
An optional argument that tells the filter how to handle the queryset. It can
|
||||
accept either a callable or the name of a method on the ``FilterSet``. The
|
||||
method receives a ``QuerySet``, the name of the model field to filter on, and
|
||||
the value to filter with. It should return a ``Queryset`` that is filtered
|
||||
appropriately.
|
||||
|
||||
The passed in value is validated and cleaned by the filter's ``field_class``,
|
||||
so raw value transformation and empty value checking should be unnecessary.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by if books are published or not"""
|
||||
published = BooleanFilter(name='published_on', method='filter_published')
|
||||
|
||||
def filter_published(self, queryset, name, value):
|
||||
# construct the full lookup expression.
|
||||
lookup = '__'.join([name, 'isnull'])
|
||||
return queryset.filter(**{lookup: False})
|
||||
|
||||
# alternatively, it may not be necessary to construct the lookup.
|
||||
return queryset.filter(published_on__isnull=False)
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['published']
|
||||
|
||||
|
||||
# Callables may also be defined out of the class scope.
|
||||
def filter_not_empty(queryset, name, value):
|
||||
lookup = '__'.join([name, 'isnull'])
|
||||
return queryset.filter(**{lookup: False})
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by if books are published or not"""
|
||||
published = BooleanFilter(name='published_on', method=filter_not_empty)
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['published']
|
||||
|
||||
``lookup_expr``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The lookup expression that should be performed using `Django's ORM`_.
|
||||
|
||||
.. _`Django's ORM`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
||||
|
||||
A ``list`` or ``tuple`` of lookup types is also accepted, allowing the user to
|
||||
select the lookup from a dropdown. The list of lookup types are filtered against
|
||||
``filters.LOOKUP_TYPES``. If `lookup_expr=None` is passed, then a list of all lookup
|
||||
types will be generated::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(lookup_expr=['exact', 'iexact'])
|
||||
|
||||
You can enable custom lookups by adding them to ``LOOKUP_TYPES``::
|
||||
|
||||
from django_filters import filters
|
||||
|
||||
filters.LOOKUP_TYPES = ['gt', 'gte', 'lt', 'lte', 'custom_lookup_type']
|
||||
|
||||
Additionally, you can provide human-friendly help text by overriding ``LOOKUP_TYPES``::
|
||||
|
||||
# filters.py
|
||||
from django_filters import filters
|
||||
|
||||
filters.LOOKUP_TYPES = [
|
||||
('', '---------'),
|
||||
('exact', 'Is equal to'),
|
||||
('not_exact', 'Is not equal to'),
|
||||
('lt', 'Lesser than'),
|
||||
('gt', 'Greater than'),
|
||||
('gte', 'Greater than or equal to'),
|
||||
('lte', 'Lesser than or equal to'),
|
||||
('startswith', 'Starts with'),
|
||||
('endswith', 'Ends with'),
|
||||
('contains', 'Contains'),
|
||||
('not_contains', 'Does not contain'),
|
||||
]
|
||||
|
||||
|
||||
``distinct``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
A boolean value that specifies whether the Filter will use distinct on the
|
||||
queryset. This option can be used to eliminate duplicate results when using filters that span related models. Defaults to ``False``.
|
||||
|
||||
``exclude``
|
||||
~~~~~~~~~~~
|
||||
|
||||
A boolean value that specifies whether the Filter should use ``filter`` or ``exclude`` on the queryset.
|
||||
Defaults to ``False``.
|
||||
|
||||
|
||||
``**kwargs``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Any additional keyword arguments are stored as the ``extra`` parameter on the filter. They are provided to the accompanying form Field and can be used to provide arguments like ``choices``.
|
||||
|
||||
|
||||
ModelChoiceFilter and ModelMultipleChoiceFilter arguments
|
||||
---------------------------------------------------------
|
||||
|
||||
These arguments apply specifically to ModelChoiceFilter and
|
||||
ModelMultipleChoiceFilter only.
|
||||
|
||||
``queryset``
|
||||
~~~~~~~~~~~~
|
||||
|
||||
``ModelChoiceFilter`` and ``ModelMultipleChoiceFilter`` require a queryset to
|
||||
operate on which must be passed as a kwarg.
|
||||
|
||||
``to_field_name``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you pass in ``to_field_name`` (which gets forwarded to the Django field),
|
||||
it will be used also in the default ``get_filter_predicate`` implementation
|
||||
as the model's attribute.
|
||||
|
||||
|
||||
Filters
|
||||
-------
|
||||
|
||||
``CharFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This filter does simple character matches, used with ``CharField`` and
|
||||
``TextField`` by default.
|
||||
|
||||
``UUIDFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This filter matches UUID values, used with ``models.UUIDField`` by default.
|
||||
|
||||
``BooleanFilter``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This filter matches a boolean, either ``True`` or ``False``, used with
|
||||
``BooleanField`` and ``NullBooleanField`` by default.
|
||||
|
||||
.. _choice-filter:
|
||||
|
||||
``ChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This filter matches values in its ``choices`` argument. The ``choices`` must be
|
||||
explicitly passed when the filter is declared on the ``FilterSet``. For example,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class User(models.Model):
|
||||
username = models.CharField(max_length=255)
|
||||
first_name = SubCharField(max_length=100)
|
||||
last_name = SubSubCharField(max_length=100)
|
||||
|
||||
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(0, 'Regular'),
|
||||
(1, 'Manager'),
|
||||
(2, 'Admin'),
|
||||
)
|
||||
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(choices=STATUS_CHOICES)
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status']
|
||||
|
||||
|
||||
``ChoiceFilter`` also has arguments that enable a choice for not filtering, as
|
||||
well as a choice for filtering by ``None`` values. Each of the arguments have a
|
||||
corresponding global setting (:doc:`/ref/settings`).
|
||||
|
||||
* ``empty_label``: The display label to use for the select choice to not filter.
|
||||
The choice may be disabled by setting this argument to ``None``. Defaults to
|
||||
``FILTERS_EMPTY_CHOICE_LABEL``.
|
||||
* ``null_label``: The display label to use for the choice to filter by ``None``
|
||||
values. The choice may be disabled by setting this argument to ``None``.
|
||||
Defaults to ``FILTERS_NULL_CHOICE_LABEL``.
|
||||
* ``null_value``: The special value to match to enable filtering by ``None``
|
||||
values. This value defaults ``FILTERS_NULL_CHOICE_VALUE`` and needs to be
|
||||
a non-empty value (``''``, ``None``, ``[]``, ``()``, ``{}``).
|
||||
|
||||
|
||||
``TypedChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The same as ``ChoiceFilter`` with the added possibility to convert value to
|
||||
match against. This could be done by using `coerce` parameter.
|
||||
An example use-case is limiting boolean choices to match against so only
|
||||
some predefined strings could be used as input of a boolean filter::
|
||||
|
||||
import django_filters
|
||||
from distutils.util import strtobool
|
||||
|
||||
BOOLEAN_CHOICES = (('false', 'False'), ('true', 'True'),)
|
||||
|
||||
class YourFilterSet(django_filters.FilterSet):
|
||||
...
|
||||
flag = django_filters.TypedChoiceFilter(choices=BOOLEAN_CHOICES,
|
||||
coerce=strtobool)
|
||||
|
||||
|
||||
``MultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The same as ``ChoiceFilter`` except the user can select multiple choices
|
||||
and the filter will form the OR of these choices by default to match items.
|
||||
The filter will form the AND of the selected choices when the ``conjoined=True``
|
||||
argument is passed to this class.
|
||||
|
||||
Multiple choices are represented in the query string by reusing the same key with
|
||||
different values (e.g. ''?status=Regular&status=Admin'').
|
||||
|
||||
``distinct`` defaults to ``True`` as to-many relationships will generally require this.
|
||||
|
||||
Advanced Use: Depending on your application logic, when all or no choices are
|
||||
selected, filtering may be a noop. In this case you may wish to avoid the
|
||||
filtering overhead, particularly of the `distinct` call.
|
||||
|
||||
Set `always_filter` to False after instantiation to enable the default `is_noop`
|
||||
test.
|
||||
|
||||
Override `is_noop` if you require a different test for your application.
|
||||
|
||||
|
||||
``TypedMultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Like ``MultipleChoiceFilter``, but in addition accepts the ``coerce`` parameter, as
|
||||
in ``TypedChoiceFilter``.
|
||||
|
||||
|
||||
``DateFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Matches on a date. Used with ``DateField`` by default.
|
||||
|
||||
|
||||
``TimeFilter``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Matches on a time. Used with ``TimeField`` by default.
|
||||
|
||||
|
||||
``DateTimeFilter``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Matches on a date and time. Used with ``DateTimeField`` by default.
|
||||
|
||||
|
||||
``IsoDateTimeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Uses ``IsoDateTimeField`` to support filtering on ISO 8601 formatted dates, as are often
|
||||
used in APIs, and are employed by default by Django REST Framework.
|
||||
|
||||
Example::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by date published, using ISO 8601 formatted dates"""
|
||||
published = IsoDateTimeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['published']
|
||||
|
||||
|
||||
``DurationFilter``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Matches on a duration. Used with ``DurationField`` by default.
|
||||
|
||||
Supports both Django ('%d %H:%M:%S.%f') and ISO 8601 formatted durations (but
|
||||
only the sections that are accepted by Python's timedelta, so no year, month,
|
||||
and week designators, e.g. 'P3DT10H22M').
|
||||
|
||||
|
||||
``ModelChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similar to a ``ChoiceFilter`` except it works with related models, used for
|
||||
``ForeignKey`` by default.
|
||||
|
||||
If automatically instantiated, ``ModelChoiceFilter`` will use the default
|
||||
``QuerySet`` for the related field. If manually instantiated you **must**
|
||||
provide the ``queryset`` kwarg.
|
||||
|
||||
Example::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for books by author"""
|
||||
author = ModelChoiceFilter(queryset=Author.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['author']
|
||||
|
||||
The ``queryset`` argument also supports callable behavior. If a callable is
|
||||
passed, it will be invoked with ``Filterset.request`` as its only argument.
|
||||
This allows you to easily filter by properties on the request object without
|
||||
having to override the ``FilterSet.__init__``.
|
||||
|
||||
.. note::
|
||||
|
||||
You should expect that the `request` object may be `None`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def departments(request):
|
||||
if request is None:
|
||||
return Department.objects.none()
|
||||
|
||||
company = request.user.company
|
||||
return company.department_set.all()
|
||||
|
||||
class EmployeeFilter(filters.FilterSet):
|
||||
department = filters.ModelChoiceFilter(queryset=departments)
|
||||
...
|
||||
|
||||
|
||||
``ModelMultipleChoiceFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similar to a ``MultipleChoiceFilter`` except it works with related models, used
|
||||
for ``ManyToManyField`` by default.
|
||||
|
||||
As with ``ModelChoiceFilter``, if automatically instantiated,
|
||||
``ModelMultipleChoiceFilter`` will use the default ``QuerySet`` for the related
|
||||
field. If manually instantiated you **must** provide the ``queryset`` kwarg.
|
||||
Like ``ModelChoiceFilter``, the ``queryset`` argument has callable behavior.
|
||||
|
||||
To use a custom field name for the lookup, you can use ``to_field_name``::
|
||||
|
||||
class FooFilter(BaseFilterSet):
|
||||
foo = django_filters.filters.ModelMultipleChoiceFilter(
|
||||
name='attr__uuid',
|
||||
to_field_name='uuid',
|
||||
queryset=Foo.objects.all(),
|
||||
)
|
||||
|
||||
If you want to use a custom queryset, e.g. to add annotated fields, this can be
|
||||
done as follows::
|
||||
|
||||
class MyMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
def get_filter_predicate(self, v):
|
||||
return {'annotated_field': v.annotated_field}
|
||||
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
qs = qs.annotate_with_custom_field()
|
||||
qs = super().filter(qs, value)
|
||||
return qs
|
||||
|
||||
foo = MyMultipleChoiceFilter(
|
||||
to_field_name='annotated_field',
|
||||
queryset=Model.objects.annotate_with_custom_field(),
|
||||
)
|
||||
|
||||
The ``annotate_with_custom_field`` method would be defined through a custom
|
||||
QuerySet, which then gets used as the model's manager::
|
||||
|
||||
class CustomQuerySet(models.QuerySet):
|
||||
def annotate_with_custom_field(self):
|
||||
return self.annotate(
|
||||
custom_field=Case(
|
||||
When(foo__isnull=False,
|
||||
then=F('foo__uuid')),
|
||||
When(bar__isnull=False,
|
||||
then=F('bar__uuid')),
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
|
||||
class MyModel(models.Model):
|
||||
objects = CustomQuerySet.as_manager()
|
||||
|
||||
|
||||
``NumberFilter``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Filters based on a numerical value, used with ``IntegerField``, ``FloatField``,
|
||||
and ``DecimalField`` by default.
|
||||
|
||||
``NumericRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Filters where a value is between two numerical values, or greater than a minimum or less
|
||||
than a maximum where only one limit value is provided. This filter is designed to work
|
||||
with the Postgres Numerical Range Fields, including ``IntegerRangeField``,
|
||||
``BigIntegerRangeField`` and ``FloatRangeField`` (available since Django 1.8). The default
|
||||
widget used is the ``RangeField``.
|
||||
|
||||
Regular field lookups are available in addition to several containment lookups, including
|
||||
``overlap``, ``contains``, and ``contained_by``. More details in the Django `docs`__.
|
||||
|
||||
__ https://docs.djangoproject.com/en/1.8/ref/contrib/postgres/fields/#querying-range-fields
|
||||
|
||||
If the lower limit value is provided, the filter automatically defaults to ``startswith``
|
||||
as the lookup and ``endswith`` if only the upper limit value is provided.
|
||||
|
||||
``RangeFilter``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Filters where a value is between two numerical values, or greater than a minimum or less than a maximum where only one limit value is provided. ::
|
||||
|
||||
class F(FilterSet):
|
||||
"""Filter for Books by Price"""
|
||||
price = RangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ['price']
|
||||
|
||||
qs = Book.objects.all().order_by('title')
|
||||
|
||||
# Range: Books between 5€ and 15€
|
||||
f = F({'price_0': '5', 'price_1': '15'}, queryset=qs)
|
||||
|
||||
# Min-Only: Books costing more the 11€
|
||||
f = F({'price_0': '11'}, queryset=qs)
|
||||
|
||||
# Max-Only: Books costing less than 19€
|
||||
f = F({'price_1': '19'}, queryset=qs)
|
||||
|
||||
|
||||
``DateRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Filter similar to the admin changelist date one, it has a number of common
|
||||
selections for working with date fields.
|
||||
|
||||
``DateFromToRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similar to a ``RangeFilter`` except it uses dates instead of numerical values. It can be used with ``DateField``. It also works with ``DateTimeField``, but takes into consideration only the date.
|
||||
|
||||
Example of using the ``DateField`` field::
|
||||
|
||||
class Comment(models.Model):
|
||||
date = models.DateField()
|
||||
time = models.TimeField()
|
||||
|
||||
class F(FilterSet):
|
||||
date = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['date']
|
||||
|
||||
# Range: Comments added between 2016-01-01 and 2016-02-01
|
||||
f = F({'date_0': '2016-01-01', 'date_1': '2016-02-01'})
|
||||
|
||||
# Min-Only: Comments added after 2016-01-01
|
||||
f = F({'date_0': '2016-01-01'})
|
||||
|
||||
# Max-Only: Comments added before 2016-02-01
|
||||
f = F({'date_1': '2016-02-01'})
|
||||
|
||||
.. note::
|
||||
When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime.
|
||||
This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it.
|
||||
|
||||
.. warning::
|
||||
If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively.
|
||||
This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware.
|
||||
|
||||
Example of using the ``DateTimeField`` field::
|
||||
|
||||
class Article(models.Model):
|
||||
published = models.DateTimeField()
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
Article.objects.create(published='2016-01-01 8:00')
|
||||
Article.objects.create(published='2016-01-20 10:00')
|
||||
Article.objects.create(published='2016-02-10 12:00')
|
||||
|
||||
# Range: Articles published between 2016-01-01 and 2016-02-01
|
||||
f = F({'published_0': '2016-01-01', 'published_1': '2016-02-01'})
|
||||
assert len(f.qs) == 2
|
||||
|
||||
# Min-Only: Articles published after 2016-01-01
|
||||
f = F({'published_0': '2016-01-01'})
|
||||
assert len(f.qs) == 3
|
||||
|
||||
# Max-Only: Articles published before 2016-02-01
|
||||
f = F({'published_1': '2016-02-01'})
|
||||
assert len(f.qs) == 2
|
||||
|
||||
``DateTimeFromToRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similar to a ``RangeFilter`` except it uses datetime format values instead of numerical values. It can be used with ``DateTimeField``.
|
||||
|
||||
Example::
|
||||
|
||||
class Article(models.Model):
|
||||
published = models.DateTimeField()
|
||||
|
||||
class F(FilterSet):
|
||||
published = DateTimeFromToRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['published']
|
||||
|
||||
Article.objects.create(published='2016-01-01 8:00')
|
||||
Article.objects.create(published='2016-01-01 9:30')
|
||||
Article.objects.create(published='2016-01-02 8:00')
|
||||
|
||||
# Range: Articles published 2016-01-01 between 8:00 and 10:00
|
||||
f = F({'published_0': '2016-01-01 8:00', 'published_1': '2016-01-01 10:00'})
|
||||
assert len(f.qs) == 2
|
||||
|
||||
# Min-Only: Articles published after 2016-01-01 8:00
|
||||
f = F({'published_0': '2016-01-01 8:00'})
|
||||
assert len(f.qs) == 3
|
||||
|
||||
# Max-Only: Articles published before 2016-01-01 10:00
|
||||
f = F({'published_1': '2016-01-01 10:00'})
|
||||
assert len(f.qs) == 2
|
||||
|
||||
``TimeRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similar to a ``RangeFilter`` except it uses time format values instead of numerical values. It can be used with ``TimeField``.
|
||||
|
||||
Example::
|
||||
|
||||
class Comment(models.Model):
|
||||
date = models.DateField()
|
||||
time = models.TimeField()
|
||||
|
||||
class F(FilterSet):
|
||||
time = TimeRangeFilter()
|
||||
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ['time']
|
||||
|
||||
# Range: Comments added between 8:00 and 10:00
|
||||
f = F({'time_0': '8:00', 'time_1': '10:00'})
|
||||
|
||||
# Min-Only: Comments added after 8:00
|
||||
f = F({'time_0': '8:00'})
|
||||
|
||||
# Max-Only: Comments added before 10:00
|
||||
f = F({'time_1': '10:00'})
|
||||
|
||||
``AllValuesFilter``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a ``ChoiceFilter`` whose choices are the current values in the
|
||||
database. So if in the DB for the given field you have values of 5, 7, and 9
|
||||
each of those is present as an option. This is similar to the default behavior
|
||||
of the admin.
|
||||
|
||||
``AllValuesMultipleFilter``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a ``MultipleChoiceFilter`` whose choices are the current values in the
|
||||
database. So if in the DB for the given field you have values of 5, 7, and 9
|
||||
each of those is present as an option. This is similar to the default behavior
|
||||
of the admin.
|
||||
|
||||
.. _base-in-filter:
|
||||
|
||||
``BaseInFilter``
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a base class used for creating IN lookup filters. It is expected that
|
||||
this filter class is used in conjunction with another filter class, as this
|
||||
class **only** validates that the incoming value is comma-separated. The secondary
|
||||
filter is then used to validate the individual values.
|
||||
|
||||
Example::
|
||||
|
||||
class NumberInFilter(BaseInFilter, NumberFilter):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
id__in = NumberInFilter(name='id', lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
User.objects.create(username='alex')
|
||||
User.objects.create(username='jacob')
|
||||
User.objects.create(username='aaron')
|
||||
User.objects.create(username='carl')
|
||||
|
||||
# In: User with IDs 1 and 3.
|
||||
f = F({'id__in': '1,3'})
|
||||
assert len(f.qs) == 2
|
||||
|
||||
``BaseRangeFilter``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is a base class used for creating RANGE lookup filters. It behaves
|
||||
identically to ``BaseInFilter`` with the exception that it expects only two
|
||||
comma-separated values.
|
||||
|
||||
Example::
|
||||
|
||||
class NumberRangeFilter(BaseInFilter, NumberFilter):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
id__range = NumberRangeFilter(name='id', lookup_expr='range')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
||||
User.objects.create(username='alex')
|
||||
User.objects.create(username='jacob')
|
||||
User.objects.create(username='aaron')
|
||||
User.objects.create(username='carl')
|
||||
|
||||
# Range: User with IDs between 1 and 3.
|
||||
f = F({'id__range': '1,3'})
|
||||
assert len(f.qs) == 3
|
||||
|
||||
|
||||
.. _ordering-filter:
|
||||
|
||||
``OrderingFilter``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enable queryset ordering. As an extension of ``ChoiceFilter`` it accepts
|
||||
two additional arguments that are used to build the ordering choices.
|
||||
|
||||
* ``fields`` is a mapping of {model field name: parameter name}. The
|
||||
parameter names are exposed in the choices and mask/alias the field
|
||||
names used in the ``order_by()`` call. Similar to field ``choices``,
|
||||
``fields`` accepts the 'list of two-tuples' syntax that retains order.
|
||||
``fields`` may also just be an iterable of strings. In this case, the
|
||||
field names simply double as the exposed parameter names.
|
||||
|
||||
* ``field_labels`` is an optional argument that allows you to customize
|
||||
the display label for the corresponding parameter. It accepts a mapping
|
||||
of {field name: human readable label}. Keep in mind that the key is the
|
||||
field name, and not the exposed parameter name.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
status = NumberFilter(name='status')
|
||||
|
||||
o = OrderingFilter(
|
||||
# tuple-mapping retains order
|
||||
fields=(
|
||||
('username', 'account'),
|
||||
('first_name', 'first_name'),
|
||||
('last_name', 'last_name'),
|
||||
),
|
||||
|
||||
# labels do not need to retain order
|
||||
field_labels={
|
||||
'username': 'User account',
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name']
|
||||
|
||||
>>> UserFilter().filters['o'].field.choices
|
||||
[
|
||||
('account', 'User account'),
|
||||
('-account', 'User account (descending)'),
|
||||
('first_name', 'First name'),
|
||||
('-first_name', 'First name (descending)'),
|
||||
('last_name', 'Last name'),
|
||||
('-last_name', 'Last name (descending)'),
|
||||
]
|
||||
|
||||
Additionally, you can just provide your own ``choices`` if you require
|
||||
explicit control over the exposed options. For example, when you might
|
||||
want to disable descending sort options.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
account = CharFilter(name='username')
|
||||
status = NumberFilter(name='status')
|
||||
|
||||
o = OrderingFilter(
|
||||
choices=(
|
||||
('account', 'Account'),
|
||||
),
|
||||
fields={
|
||||
'username': 'account',
|
||||
},
|
||||
)
|
||||
|
||||
This filter is also CSV-based, and accepts multiple ordering params. The
|
||||
default select widget does not enable the use of this, but it is useful
|
||||
for APIs. ``SelectMultiple`` widgets are not compatible, given that they
|
||||
are not able to retain selection order.
|
||||
|
||||
Adding Custom filter choices
|
||||
""""""""""""""""""""""""""""
|
||||
|
||||
If you wish to sort by non-model fields, you'll need to add custom handling to an
|
||||
``OrderingFilter`` subclass. For example, if you want to sort by a computed
|
||||
'relevance' factor, you would need to do something like the following:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomOrderingFilter(django_filters.OrderingFilter):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomOrderingFilter, self).__init__(*args, **kwargs)
|
||||
self.extra['choices'] += [
|
||||
('relevance', 'Relevance'),
|
||||
('-relevance', 'Relevance (descending)'),
|
||||
]
|
||||
|
||||
|
||||
def filter(self, qs, value):
|
||||
# OrderingFilter is CSV-based, so `value` is a list
|
||||
if any(v in ['relevance', '-relevance'] for v in value):
|
||||
# sort queryset by relevance
|
||||
return ...
|
||||
|
||||
return super(CustomOrderingFilter, self).filter(qs, value)
|
|
@ -0,0 +1,215 @@
|
|||
=================
|
||||
FilterSet Options
|
||||
=================
|
||||
|
||||
This document provides a guide on using additional FilterSet features.
|
||||
|
||||
Meta options
|
||||
------------
|
||||
|
||||
- :ref:`model <model>`
|
||||
- :ref:`fields <fields>`
|
||||
- :ref:`exclude <exclude>`
|
||||
- :ref:`form <form>`
|
||||
- :ref:`together <together>`
|
||||
- :ref:`filter_overrides <filter_overrides>`
|
||||
- :ref:`strict <strict>`
|
||||
|
||||
|
||||
.. _model:
|
||||
|
||||
Automatic filter generation with ``model``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``FilterSet`` is capable of automatically generating filters for a given
|
||||
``model``'s fields. Similar to Django's ``ModelForm``, filters are created
|
||||
based on the underlying model field's type. This option must be combined with
|
||||
either the ``fields`` or ``exclude`` option, which is the same requirement for
|
||||
Django's ``ModelForm`` class, detailed `here`__.
|
||||
|
||||
__ https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#selecting-the-fields-to-use
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'last_login']
|
||||
|
||||
|
||||
.. _fields:
|
||||
|
||||
Declaring filterable ``fields``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``fields`` option is combined with ``model`` to automatically generate
|
||||
filters. Note that generated filters will not overwrite filters declared on
|
||||
the ``FilterSet``. The ``fields`` option accepts two syntaxes:
|
||||
|
||||
* a list of field names
|
||||
* a dictionary of field names mapped to a list of lookups
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'last_login']
|
||||
|
||||
# or
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
'username': ['exact', 'contains'],
|
||||
'last_login': ['exact', 'year__gt'],
|
||||
}
|
||||
|
||||
The list syntax will create an ``exact`` lookup filter for each field included
|
||||
in ``fields``. The dictionary syntax will create a filter for each lookup
|
||||
expression declared for its corresponding model field. These expressions may
|
||||
include both transforms and lookups, as detailed in the `lookup reference`__.
|
||||
|
||||
__ https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups
|
||||
|
||||
|
||||
.. _exclude:
|
||||
|
||||
Disable filter fields with ``exclude``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``exclude`` option accepts a blacklist of field names to exclude from
|
||||
automatic filter generation. Note that this option will not disable filters
|
||||
declared directly on the ``FilterSet``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class UserFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ['password']
|
||||
|
||||
|
||||
.. _form:
|
||||
|
||||
Custom Forms using ``form``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The inner ``Meta`` class also takes an optional ``form`` argument. This is a
|
||||
form class from which ``FilterSet.form`` will subclass. This works similar to
|
||||
the ``form`` option on a ``ModelAdmin.``
|
||||
|
||||
|
||||
.. _together:
|
||||
|
||||
Group fields with ``together``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
||||
is a list of lists, each containing field names. For convenience can be a
|
||||
single list/tuple when dealing with a single set of fields. Fields within a
|
||||
field set must either be all or none present in the request for
|
||||
``FilterSet.form`` to be valid::
|
||||
|
||||
import django_filters
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['price', 'release_date', 'rating']
|
||||
together = ['rating', 'price']
|
||||
|
||||
|
||||
.. _filter_overrides:
|
||||
|
||||
Customise filter generation with ``filter_overrides``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The inner ``Meta`` class also takes an optional ``filter_overrides`` argument.
|
||||
This is a map of model fields to filter classes with options::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['name', 'release_date']
|
||||
filter_overrides = {
|
||||
models.CharField: {
|
||||
'filter_class': django_filters.CharFilter,
|
||||
'extra': lambda f: {
|
||||
'lookup_expr': 'icontains',
|
||||
},
|
||||
},
|
||||
models.BooleanField: {
|
||||
'filter_class': django_filters.BooleanFilter,
|
||||
'extra': lambda f: {
|
||||
'widget': forms.CheckboxInput,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
.. _strict:
|
||||
|
||||
Handling validation errors with ``strict``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``strict`` option determines the filterset's behavior when filters fail to validate. Example use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django_filters import FilterSet, STRICTNESS
|
||||
|
||||
class ProductFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['name', 'release_date']
|
||||
strict = STRICTNESS.RETURN_NO_RESULTS
|
||||
|
||||
Currently, there are three different behaviors:
|
||||
|
||||
- ``STRICTNESS.RETURN_NO_RESULTS`` (default) This returns an empty queryset. The
|
||||
filterset form can then be rendered to display the input errors.
|
||||
- ``STRICTNESS.IGNORE`` Instead of returning an empty queryset, invalid filters
|
||||
effectively become a noop. Valid filters are applied to the queryset however.
|
||||
- ``STRICTNESS.RAISE_VALIDATION_ERROR`` This raises a ``ValidationError`` for
|
||||
all invalid filters. This behavior is generally useful with APIs.
|
||||
|
||||
If the ``strict`` option is not provided, then the filterset will default to the
|
||||
value of the ``FILTERS_STRICTNESS`` setting.
|
||||
|
||||
|
||||
Overriding ``FilterSet`` methods
|
||||
--------------------------------
|
||||
|
||||
``filter_for_lookup()``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Prior to version 0.13.0, filter generation did not take into account the
|
||||
``lookup_expr`` used. This commonly caused malformed filters to be generated
|
||||
for 'isnull', 'in', and 'range' lookups (as well as transformed lookups). The
|
||||
current implementation provides the following behavior:
|
||||
|
||||
- 'isnull' lookups return a ``BooleanFilter``
|
||||
- 'in' lookups return a filter derived from the CSV-based ``BaseInFilter``.
|
||||
- 'range' lookups return a filter derived from the CSV-based ``BaseRangeFilter``.
|
||||
|
||||
If you want to override the ``filter_class`` and ``params`` used to instantiate
|
||||
filters for a model field, you can override ``filter_for_lookup()``. Ex::
|
||||
|
||||
class ProductFilter(django_filters.FilterSet):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = {
|
||||
'release_date': ['exact', 'range'],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def filter_for_lookup(cls, f, lookup_type):
|
||||
# override date range lookups
|
||||
if isinstance(f, models.DateField) and lookup_type == 'range':
|
||||
return django_filters.DateRangeFilter, {}
|
||||
|
||||
# use default behavior otherwise
|
||||
return super(ProductFilter, cls).filter_for_lookup(f, lookup_type)
|
|
@ -0,0 +1,106 @@
|
|||
==================
|
||||
Settings Reference
|
||||
==================
|
||||
|
||||
Here is a list of all available settings of django-filters and their
|
||||
default values. All settings are prefixed with ``FILTERS_``, although this
|
||||
is a bit verbose it helps to make it easy to identify these settings.
|
||||
|
||||
|
||||
FILTERS_EMPTY_CHOICE_LABEL
|
||||
--------------------------
|
||||
|
||||
Default: ``'---------'``
|
||||
|
||||
Set the default value for ``ChoiceFilter.empty_label``. You may disable the empty choice by setting this to ``None``.
|
||||
|
||||
|
||||
FILTERS_NULL_CHOICE_LABEL
|
||||
-------------------------
|
||||
|
||||
Default: ``None``
|
||||
|
||||
Set the default value for ``ChoiceFilter.null_label``. You may enable the null choice by setting a non-``None`` value.
|
||||
|
||||
|
||||
FILTERS_NULL_CHOICE_VALUE
|
||||
-------------------------
|
||||
|
||||
Default: ``'null'``
|
||||
|
||||
Set the default value for ``ChoiceFilter.null_value``. You may want to change this value if the default ``'null'`` string conflicts with an actual choice.
|
||||
|
||||
|
||||
FILTERS_DISABLE_HELP_TEXT
|
||||
-------------------------
|
||||
|
||||
Default: ``False``
|
||||
|
||||
Some filters provide informational ``help_text``. For example, csv-based
|
||||
filters (``filters.BaseCSVFilter``) inform users that "Multiple values may
|
||||
be separated by commas".
|
||||
|
||||
You may set this to ``True`` to disable the ``help_text`` for **all**
|
||||
filters, removing the text from the rendered form's output.
|
||||
|
||||
|
||||
.. _verbose-lookups-setting:
|
||||
|
||||
FILTERS_VERBOSE_LOOKUPS
|
||||
-----------------------
|
||||
|
||||
.. note::
|
||||
|
||||
This is considered an advanced setting and is subject to change.
|
||||
|
||||
Default:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# refer to 'django_filters.conf.DEFAULTS'
|
||||
'VERBOSE_LOOKUPS': {
|
||||
'exact': _(''),
|
||||
'iexact': _(''),
|
||||
'contains': _('contains'),
|
||||
'icontains': _('contains'),
|
||||
...
|
||||
}
|
||||
|
||||
This setting controls the verbose output for generated filter labels. Instead
|
||||
of getting expression parts such as "lt" and "contained_by", the verbose label
|
||||
would contain "is less than" and "is contained by". Verbose output may be
|
||||
disabled by setting this to a falsy value.
|
||||
|
||||
This setting also accepts callables. The callable should not require arguments
|
||||
and should return a dictionary. This is useful for extending or overriding the
|
||||
default terms without having to copy the entire set of terms to your settings.
|
||||
For example, you could add verbose output for "exact" lookups.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
def FILTERS_VERBOSE_LOOKUPS():
|
||||
from django_filters.conf import DEFAULTS
|
||||
|
||||
verbose_lookups = DEFAULTS['VERBOSE_LOOKUPS'].copy()
|
||||
verbose_lookups.update({
|
||||
'exact': 'is equal to',
|
||||
})
|
||||
return verbose_lookups
|
||||
|
||||
|
||||
FILTERS_STRICTNESS
|
||||
------------------
|
||||
|
||||
Default: ``STRICTNESS.RETURN_NO_RESULTS``
|
||||
|
||||
Set the global default for FilterSet :ref:`strictness <strict>`. If ``strict`` is not
|
||||
provided to the filterset, it will default to this setting. You can change the setting
|
||||
like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
from django_filters import STRICTNESS
|
||||
|
||||
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS
|
|
@ -0,0 +1,88 @@
|
|||
================
|
||||
Widget Reference
|
||||
================
|
||||
|
||||
This is a reference document with a list of the provided widgets and their
|
||||
arguments.
|
||||
|
||||
|
||||
.. _link-widget:
|
||||
|
||||
``LinkWidget``
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This widget renders each option as a link, instead of an actual <input>. It has
|
||||
one method that you can override for additional customizability.
|
||||
``option_string()`` should return a string with 3 Python keyword argument
|
||||
placeholders:
|
||||
|
||||
1. ``attrs``: This is a string with all the attributes that will be on the
|
||||
final ``<a>`` tag.
|
||||
2. ``query_string``: This is the query string for use in the ``href``
|
||||
option on the ``<a>`` element.
|
||||
3. ``label``: This is the text to be displayed to the user.
|
||||
|
||||
|
||||
.. _boolean-widget:
|
||||
|
||||
``BooleanWidget``
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This widget converts its input into Python's True/False values. It will convert
|
||||
all case variations of ``True`` and ``False`` into the internal Python values.
|
||||
To use it, pass this into the ``widgets`` argument of the ``BooleanFilter``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
active = BooleanFilter(widget=BooleanWidget())
|
||||
|
||||
|
||||
.. _csv-widget:
|
||||
|
||||
``CSVWidget``
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
This widget expects a comma separated value and converts it into a list of
|
||||
string values. It is expected that the field class handle a list of values as
|
||||
well as type conversion.
|
||||
|
||||
|
||||
.. _range-widget:
|
||||
|
||||
``RangeWidget``
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This widget is used with ``RangeFilter`` and its subclasses. It generates two
|
||||
form input elements which generally act as start/end values in a range.
|
||||
Under the hood, it is django's ``forms.TextInput`` widget and excepts
|
||||
the same arguments and values. To use it, pass it to ``widget`` argument of
|
||||
a ``RangeField``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'}))
|
||||
|
||||
|
||||
``SuffixedMultiWidget``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Extends Django's builtin ``MultiWidget`` to append custom suffixes instead of
|
||||
indices. For example, take a range widget that accepts minimum and maximum
|
||||
bounds. By default, the resulting query params would look like the following:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /products?price_0=10&price_1=25 HTTP/1.1
|
||||
|
||||
By using ``SuffixedMultiWidget`` instead, you can provide human-friendly suffixes.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class RangeWidget(SuffixedMultiWidget):
|
||||
suffixes = ['min', 'max']
|
||||
|
||||
The query names are now a little more ergonomic.
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /products?price_min=10&price_max=25 HTTP/1.1
|
|
@ -0,0 +1,27 @@
|
|||
alabaster==0.7.7
|
||||
argh==0.26.1
|
||||
Babel==2.2.0
|
||||
backports.ssl-match-hostname==3.4.0.2
|
||||
bumpversion==0.5.3
|
||||
certifi==2015.9.6.2
|
||||
docutils==0.12
|
||||
funcsigs==0.4
|
||||
Jinja2==2.8
|
||||
livereload==2.4.0
|
||||
MarkupSafe==0.23
|
||||
pathtools==0.1.2
|
||||
pbr==1.7.0
|
||||
pkginfo==1.2.1
|
||||
Pygments==2.1.3
|
||||
pytz==2016.6.1
|
||||
PyYAML==3.11
|
||||
requests==2.9.1
|
||||
requests-toolbelt==0.6.0
|
||||
six==1.9.0
|
||||
snowballstemmer==1.2.1
|
||||
Sphinx==1.3.6
|
||||
sphinx-autobuild==0.6.0
|
||||
sphinx-rtd-theme==0.1.9
|
||||
tornado==4.2.1
|
||||
twine==1.6.5
|
||||
watchdog==0.8.3
|
|
@ -0,0 +1,7 @@
|
|||
markdown==2.6.4
|
||||
coreapi
|
||||
django-crispy-forms
|
||||
|
||||
coverage
|
||||
mock
|
||||
pytz
|
|
@ -0,0 +1,3 @@
|
|||
-r test-ci.txt
|
||||
django
|
||||
djangorestframework
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
||||
def runshell():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
|
||||
execute_from_command_line(
|
||||
sys.argv[:1] +
|
||||
['migrate', '--noinput', '-v', '0'] +
|
||||
(['--run-syncdb'] if django.VERSION >= (1, 9) else []))
|
||||
|
||||
argv = sys.argv[:1] + ['shell'] + sys.argv[1:]
|
||||
execute_from_command_line(argv)
|
||||
|
||||
if __name__ == '__main__':
|
||||
runshell()
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
||||
def runtests():
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
|
||||
argv = sys.argv[:1] + ['test'] + sys.argv[1:]
|
||||
execute_from_command_line(argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
runtests()
|
|
@ -0,0 +1,19 @@
|
|||
[metadata]
|
||||
license-file = LICENSE
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[isort]
|
||||
skip = .tox
|
||||
atomic = true
|
||||
multi_line_output = 3
|
||||
known_standard_library = mock
|
||||
known_third_party = django,pytz,rest_framework
|
||||
known_first_party = django_filters
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
tag_svn_revision = 0
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import os
|
||||
import sys
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
f = open('README.rst')
|
||||
readme = f.read()
|
||||
f.close()
|
||||
|
||||
version = '1.1.0'
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
if os.system("pip freeze | grep wheel"):
|
||||
print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
|
||||
sys.exit()
|
||||
if os.system("pip freeze | grep twine"):
|
||||
print("twine not installed.\nUse `pip install twine`.\nExiting.")
|
||||
sys.exit()
|
||||
os.system("python setup.py sdist bdist_wheel")
|
||||
os.system("twine upload dist/*")
|
||||
print("You probably want to also tag the version now:")
|
||||
print(" git tag -a %s -m 'version %s'" % (version, version))
|
||||
print(" git push --tags")
|
||||
sys.exit()
|
||||
|
||||
setup(
|
||||
name='django-filter',
|
||||
version=version,
|
||||
description=('Django-filter is a reusable Django application for allowing'
|
||||
' users to filter querysets dynamically.'),
|
||||
long_description=readme,
|
||||
author='Alex Gaynor',
|
||||
author_email='alex.gaynor@gmail.com',
|
||||
maintainer='Carlton Gibson',
|
||||
maintainer_email='carlton.gibson@noumenal.es',
|
||||
url='https://github.com/carltongibson/django-filter/tree/master',
|
||||
packages=find_packages(exclude=['tests*']),
|
||||
include_package_data=True,
|
||||
license='BSD',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
],
|
||||
zip_safe=False,
|
||||
)
|
|
@ -0,0 +1,214 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
REGULAR = 0
|
||||
MANAGER = 1
|
||||
ADMIN = 2
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(REGULAR, 'Regular'),
|
||||
(MANAGER, 'Manager'),
|
||||
(ADMIN, 'Admin'),
|
||||
)
|
||||
|
||||
|
||||
# classes for testing filters with inherited fields
|
||||
class SubCharField(models.CharField):
|
||||
pass
|
||||
|
||||
|
||||
class SubSubCharField(SubCharField):
|
||||
pass
|
||||
|
||||
|
||||
class SubnetMaskField(models.Field):
|
||||
empty_strings_allowed = False
|
||||
description = "Subnet Mask"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = 15
|
||||
models.Field.__init__(self, *args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "GenericIPAddressField"
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.GenericIPAddressField}
|
||||
defaults.update(kwargs)
|
||||
return super(SubnetMaskField, self).formfield(**defaults)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class User(models.Model):
|
||||
username = models.CharField(_('username'), max_length=255)
|
||||
first_name = SubCharField(max_length=100)
|
||||
last_name = SubSubCharField(max_length=100)
|
||||
|
||||
status = models.IntegerField(choices=STATUS_CHOICES, default=0)
|
||||
|
||||
is_active = models.BooleanField(default=False)
|
||||
|
||||
favorite_books = models.ManyToManyField('Book', related_name='lovers')
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ManagerGroup(models.Model):
|
||||
users = models.ManyToManyField(User,
|
||||
limit_choices_to={'is_active': True},
|
||||
related_name='member_of')
|
||||
manager = models.ForeignKey(User,
|
||||
limit_choices_to=lambda: {'status': MANAGER},
|
||||
related_name='manager_of',
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
return self.manager.name + ' group'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AdminUser(User):
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def __str__(self):
|
||||
return "%s (ADMIN)" % self.username
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Comment(models.Model):
|
||||
text = models.TextField()
|
||||
author = models.ForeignKey(User, related_name='comments', on_delete=models.CASCADE)
|
||||
|
||||
date = models.DateField()
|
||||
time = models.TimeField()
|
||||
|
||||
def __str__(self):
|
||||
return "%s said %s" % (self.author, self.text[:25])
|
||||
|
||||
|
||||
class Article(models.Model):
|
||||
name = models.CharField(verbose_name='title', max_length=200, blank=True)
|
||||
published = models.DateTimeField()
|
||||
author = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
|
||||
|
||||
def __str__(self):
|
||||
if self.author_id:
|
||||
return "%s on %s" % (self.author, self.published)
|
||||
return "Anonymous on %s" % self.published
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Book(models.Model):
|
||||
title = models.CharField(max_length=100)
|
||||
price = models.DecimalField(max_digits=6, decimal_places=2)
|
||||
average_rating = models.FloatField()
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class Place(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Restaurant(Place):
|
||||
serves_pizza = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class NetworkSetting(models.Model):
|
||||
ip = models.GenericIPAddressField()
|
||||
mask = SubnetMaskField()
|
||||
|
||||
cidr = models.CharField(max_length=18, blank=True, verbose_name="CIDR")
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Company(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Location(models.Model):
|
||||
company = models.ForeignKey(Company, related_name='locations', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=100)
|
||||
zip_code = models.CharField(max_length=10)
|
||||
open_days = models.CharField(max_length=7)
|
||||
|
||||
def __str__(self):
|
||||
return '%s: %s' % (self.company.name, self.name)
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
in_good_standing = models.BooleanField(default=False)
|
||||
friendly = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
account = models.OneToOneField(Account, related_name='profile', on_delete=models.CASCADE)
|
||||
likes_coffee = models.BooleanField(default=False)
|
||||
likes_tea = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class BankAccount(Account):
|
||||
amount_saved = models.IntegerField(default=0)
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
adjacents = models.ManyToManyField('self')
|
||||
|
||||
|
||||
class DirectedNode(models.Model):
|
||||
name = models.CharField(max_length=20)
|
||||
outbound_nodes = models.ManyToManyField('self',
|
||||
symmetrical=False,
|
||||
related_name='inbound_nodes')
|
||||
|
||||
|
||||
class Worker(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class HiredWorker(models.Model):
|
||||
salary = models.IntegerField()
|
||||
hired_on = models.DateField()
|
||||
worker = models.ForeignKey(Worker, on_delete=models.CASCADE)
|
||||
business = models.ForeignKey('Business', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Business(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
employees = models.ManyToManyField(Worker,
|
||||
through=HiredWorker,
|
||||
related_name='employers')
|
||||
|
||||
|
||||
class UUIDTestModel(models.Model):
|
||||
uuid = models.UUIDField()
|
||||
|
||||
|
||||
class SpacewalkRecord(models.Model):
|
||||
"""Cumulative space walk record.
|
||||
|
||||
See: https://en.wikipedia.org/wiki/List_of_cumulative_spacewalk_records
|
||||
|
||||
"""
|
||||
astronaut = models.CharField(max_length=100)
|
||||
duration = models.DurationField()
|
|
@ -0,0 +1 @@
|
|||
default_app_config = 'tests.rest_framework.apps.RestFrameworkTestConfig'
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RestFrameworkTestConfig(AppConfig):
|
||||
name = 'tests.rest_framework'
|
||||
label = 'drf_test_app'
|
||||
verbose_name = "Rest Framework Test App"
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class BasicModel(models.Model):
|
||||
text = models.CharField(
|
||||
max_length=100,
|
||||
verbose_name=_("Text comes here"),
|
||||
help_text=_("Text description.")
|
||||
)
|
||||
|
||||
|
||||
class BaseFilterableItem(models.Model):
|
||||
text = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class FilterableItem(BaseFilterableItem):
|
||||
decimal = models.DecimalField(max_digits=4, decimal_places=2)
|
||||
date = models.DateField()
|
||||
|
||||
|
||||
class DjangoFilterOrderingModel(models.Model):
|
||||
date = models.DateField()
|
||||
text = models.CharField(max_length=10)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-date']
|
|
@ -0,0 +1 @@
|
|||
Test
|
|
@ -0,0 +1,229 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import warnings
|
||||
from decimal import Decimal
|
||||
from unittest import skipIf
|
||||
|
||||
from django.db.models import BooleanField
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from rest_framework import generics, serializers
|
||||
from rest_framework.test import APIRequestFactory
|
||||
|
||||
from django_filters import compat, filters
|
||||
from django_filters.rest_framework import (
|
||||
DjangoFilterBackend,
|
||||
FilterSet,
|
||||
backends
|
||||
)
|
||||
|
||||
from .models import FilterableItem
|
||||
|
||||
factory = APIRequestFactory()
|
||||
|
||||
|
||||
class FilterableItemSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
# Basic filter on a list view.
|
||||
class FilterFieldsRootView(generics.ListCreateAPIView):
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_fields = ['decimal', 'date']
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
|
||||
|
||||
# These class are used to test a filter class.
|
||||
class SeveralFieldsFilter(FilterSet):
|
||||
text = filters.CharFilter(lookup_expr='icontains')
|
||||
decimal = filters.NumberFilter(lookup_expr='lt')
|
||||
date = filters.DateFilter(lookup_expr='gt')
|
||||
|
||||
class Meta:
|
||||
model = FilterableItem
|
||||
fields = ['text', 'decimal', 'date']
|
||||
|
||||
|
||||
class FilterClassRootView(generics.ListCreateAPIView):
|
||||
queryset = FilterableItem.objects.all()
|
||||
serializer_class = FilterableItemSerializer
|
||||
filter_class = SeveralFieldsFilter
|
||||
filter_backends = (DjangoFilterBackend,)
|
||||
|
||||
|
||||
@skipIf(compat.coreapi is None, 'coreapi must be installed')
|
||||
class GetSchemaFieldsTests(TestCase):
|
||||
def test_fields_with_filter_fields_list(self):
|
||||
backend = DjangoFilterBackend()
|
||||
fields = backend.get_schema_fields(FilterFieldsRootView())
|
||||
fields = [f.name for f in fields]
|
||||
|
||||
self.assertEqual(fields, ['decimal', 'date'])
|
||||
|
||||
def test_filter_fields_list_with_bad_get_queryset(self):
|
||||
"""
|
||||
See:
|
||||
* https://github.com/carltongibson/django-filter/issues/551
|
||||
"""
|
||||
class BadGetQuerySetView(FilterFieldsRootView):
|
||||
def get_queryset(self):
|
||||
raise AttributeError("I don't have that")
|
||||
|
||||
backend = DjangoFilterBackend()
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
|
||||
fields = backend.get_schema_fields(BadGetQuerySetView())
|
||||
self.assertEqual(fields, [], "get_schema_fields should handle AttributeError")
|
||||
|
||||
warning = "{} is not compatible with schema generation".format(BadGetQuerySetView)
|
||||
self.assertEqual(len(w), 1)
|
||||
self.assertEqual(str(w[0].message), warning)
|
||||
|
||||
def test_fields_with_filter_fields_dict(self):
|
||||
class DictFilterFieldsRootView(FilterFieldsRootView):
|
||||
filter_fields = {
|
||||
'decimal': ['exact', 'lt', 'gt'],
|
||||
}
|
||||
|
||||
backend = DjangoFilterBackend()
|
||||
fields = backend.get_schema_fields(DictFilterFieldsRootView())
|
||||
fields = [f.name for f in fields]
|
||||
|
||||
self.assertEqual(fields, ['decimal', 'decimal__lt', 'decimal__gt'])
|
||||
|
||||
def test_fields_with_filter_class(self):
|
||||
backend = DjangoFilterBackend()
|
||||
fields = backend.get_schema_fields(FilterClassRootView())
|
||||
schemas = [f.schema for f in fields]
|
||||
fields = [f.name for f in fields]
|
||||
|
||||
self.assertEqual(fields, ['text', 'decimal', 'date'])
|
||||
self.assertIsInstance(schemas[0], compat.coreschema.String)
|
||||
self.assertIsInstance(schemas[1], compat.coreschema.Number)
|
||||
self.assertIsInstance(schemas[2], compat.coreschema.String)
|
||||
|
||||
def test_field_required(self):
|
||||
class RequiredFieldsFilter(SeveralFieldsFilter):
|
||||
required_text = filters.CharFilter(required=True)
|
||||
|
||||
class Meta(SeveralFieldsFilter.Meta):
|
||||
fields = SeveralFieldsFilter.Meta.fields + ['required_text']
|
||||
|
||||
class FilterClassWithRequiredFieldsView(FilterClassRootView):
|
||||
filter_class = RequiredFieldsFilter
|
||||
|
||||
backend = DjangoFilterBackend()
|
||||
fields = backend.get_schema_fields(FilterClassWithRequiredFieldsView())
|
||||
required = [f.required for f in fields]
|
||||
fields = [f.name for f in fields]
|
||||
|
||||
self.assertEqual(fields, ['text', 'decimal', 'date', 'required_text'])
|
||||
self.assertFalse(required[0])
|
||||
self.assertFalse(required[1])
|
||||
self.assertFalse(required[2])
|
||||
self.assertTrue(required[3])
|
||||
|
||||
def tests_field_with_request_callable(self):
|
||||
def qs(request):
|
||||
# users expect a valid request object to be provided which cannot
|
||||
# be guaranteed during schema generation.
|
||||
self.fail("callable queryset should not be invoked during schema generation")
|
||||
|
||||
class F(SeveralFieldsFilter):
|
||||
f = filters.ModelChoiceFilter(queryset=qs)
|
||||
|
||||
class View(FilterClassRootView):
|
||||
filter_class = F
|
||||
|
||||
view = View()
|
||||
view.request = factory.get('/')
|
||||
backend = DjangoFilterBackend()
|
||||
fields = backend.get_schema_fields(view)
|
||||
fields = [f.name for f in fields]
|
||||
|
||||
self.assertEqual(fields, ['text', 'decimal', 'date', 'f'])
|
||||
|
||||
|
||||
class TemplateTests(TestCase):
|
||||
def test_backend_output(self):
|
||||
"""
|
||||
Ensure backend renders default if template path does not exist
|
||||
"""
|
||||
view = FilterFieldsRootView()
|
||||
backend = view.filter_backends[0]
|
||||
request = view.initialize_request(factory.get('/'))
|
||||
html = backend().to_html(request, view.get_queryset(), view)
|
||||
|
||||
self.assertHTMLEqual(html, """
|
||||
<h2>Field filters</h2>
|
||||
<form class="form" action="" method="get">
|
||||
<p>
|
||||
<label for="id_decimal">Decimal:</label>
|
||||
<input id="id_decimal" name="decimal" step="any" type="number" />
|
||||
</p>
|
||||
<p>
|
||||
<label for="id_date">Date:</label>
|
||||
<input id="id_date" name="date" type="text" />
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
""")
|
||||
|
||||
def test_template_path(self):
|
||||
view = FilterFieldsRootView()
|
||||
|
||||
class Backend(view.filter_backends[0]):
|
||||
template = 'filter_template.html'
|
||||
|
||||
request = view.initialize_request(factory.get('/'))
|
||||
html = Backend().to_html(request, view.get_queryset(), view)
|
||||
|
||||
self.assertHTMLEqual(html, "Test")
|
||||
|
||||
@override_settings(TEMPLATES=[])
|
||||
def test_DTL_missing(self):
|
||||
# The backend should be importable even if the DTL is not used.
|
||||
# See: https://github.com/carltongibson/django-filter/issues/506
|
||||
try:
|
||||
from importlib import reload # python 3.4
|
||||
except ImportError:
|
||||
from imp import reload
|
||||
|
||||
reload(backends)
|
||||
|
||||
def test_multiple_engines(self):
|
||||
# See: https://github.com/carltongibson/django-filter/issues/578
|
||||
DTL = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True}
|
||||
ALT = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'NAME': 'alt'}
|
||||
|
||||
# multiple DTL backends
|
||||
with override_settings(TEMPLATES=[DTL, ALT]):
|
||||
self.test_backend_output()
|
||||
|
||||
|
||||
class DefaultFilterSetTests(TestCase):
|
||||
def test_default_meta_inheritance(self):
|
||||
# https://github.com/carltongibson/django-filter/issues/663
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
filter_overrides = {BooleanField: {}}
|
||||
|
||||
class Backend(DjangoFilterBackend):
|
||||
default_filter_set = F
|
||||
|
||||
view = FilterFieldsRootView()
|
||||
backend = Backend()
|
||||
|
||||
filter_class = backend.get_filter_class(view, view.get_queryset())
|
||||
filter_overrides = filter_class._meta.filter_overrides
|
||||
|
||||
# derived filter_class.Meta should inherit from default_filter_set.Meta
|
||||
self.assertIn(BooleanField, filter_overrides)
|
||||
self.assertDictEqual(filter_overrides[BooleanField], {})
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
from django.test import TestCase
|
||||
|
||||
from django_filters.rest_framework import filters
|
||||
from django_filters.widgets import BooleanWidget
|
||||
|
||||
|
||||
class BooleanFilterTests(TestCase):
|
||||
|
||||
def test_widget(self):
|
||||
# Ensure that `BooleanFilter` uses the correct widget when importing
|
||||
# from `rest_framework.filters`.
|
||||
f = filters.BooleanFilter()
|
||||
|
||||
self.assertEqual(f.extra['widget'], BooleanWidget)
|
|
@ -0,0 +1,45 @@
|
|||
from unittest import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from django_filters.compat import is_crispy
|
||||
from django_filters.rest_framework import FilterSet, filters
|
||||
from django_filters.widgets import BooleanWidget
|
||||
|
||||
from ..models import Article, User
|
||||
|
||||
|
||||
class ArticleFilter(FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['author']
|
||||
|
||||
|
||||
class FilterSetFilterForFieldTests(TestCase):
|
||||
|
||||
def test_isodatetimefilter(self):
|
||||
field = Article._meta.get_field('published')
|
||||
result = FilterSet.filter_for_field(field, 'published')
|
||||
self.assertIsInstance(result, filters.IsoDateTimeFilter)
|
||||
self.assertEqual(result.name, 'published')
|
||||
|
||||
def test_booleanfilter_widget(self):
|
||||
field = User._meta.get_field('is_active')
|
||||
result = FilterSet.filter_for_field(field, 'is_active')
|
||||
self.assertIsInstance(result, filters.BooleanFilter)
|
||||
self.assertEqual(result.extra['widget'], BooleanWidget)
|
||||
|
||||
|
||||
@skipIf(is_crispy(), 'django_crispy_forms must be installed')
|
||||
@override_settings(INSTALLED_APPS=settings.INSTALLED_APPS + ('crispy_forms', ))
|
||||
class CrispyFormsCompatTests(TestCase):
|
||||
|
||||
def test_crispy_helper(self):
|
||||
# ensure the helper is present on the form
|
||||
self.assertTrue(hasattr(ArticleFilter().form, 'helper'))
|
||||
|
||||
def test_form_initialization(self):
|
||||
# ensure that crispy compat does not prematurely initialize the form
|
||||
self.assertFalse(hasattr(ArticleFilter(), '_form'))
|
|
@ -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'}
|
||||
]
|
||||
)
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
# ensure package/conf is importable
|
||||
from django_filters import STRICTNESS
|
||||
from django_filters.conf import DEFAULTS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': ':memory:',
|
||||
},
|
||||
}
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.auth',
|
||||
'rest_framework',
|
||||
'django_filters',
|
||||
'tests.rest_framework',
|
||||
'tests',
|
||||
)
|
||||
|
||||
MIDDLEWARE = []
|
||||
|
||||
ROOT_URLCONF = 'tests.urls'
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
SECRET_KEY = 'foobar'
|
||||
|
||||
TEMPLATES = [{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'APP_DIRS': True,
|
||||
}]
|
||||
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
|
||||
# help verify that DEFAULTS is importable from conf.
|
||||
def FILTERS_VERBOSE_LOOKUPS():
|
||||
return DEFAULTS['VERBOSE_LOOKUPS']
|
||||
|
||||
|
||||
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{{ filter.form }}
|
||||
|
||||
{% for obj in filter.qs %}
|
||||
{{ obj }}
|
||||
{% endfor %}
|
|
@ -0,0 +1,141 @@
|
|||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters import STRICTNESS, FilterSet
|
||||
from django_filters.conf import is_callable, settings
|
||||
from tests.models import User
|
||||
|
||||
|
||||
class DefaultSettingsTests(TestCase):
|
||||
|
||||
def test_verbose_lookups(self):
|
||||
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
|
||||
self.assertIn('exact', settings.VERBOSE_LOOKUPS)
|
||||
|
||||
def test_disable_help_text(self):
|
||||
self.assertFalse(settings.DISABLE_HELP_TEXT)
|
||||
|
||||
def test_strictness(self):
|
||||
self.assertEqual(settings.STRICTNESS, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_help_text_filter(self):
|
||||
self.assertTrue(settings.HELP_TEXT_FILTER)
|
||||
|
||||
def test_help_text_exclude(self):
|
||||
self.assertTrue(settings.HELP_TEXT_EXCLUDE)
|
||||
|
||||
def test_empty_choice_label(self):
|
||||
self.assertEqual(settings.EMPTY_CHOICE_LABEL, '---------')
|
||||
|
||||
def test_null_choice_label(self):
|
||||
self.assertIsNone(settings.NULL_CHOICE_LABEL)
|
||||
|
||||
def test_null_choice_value(self):
|
||||
self.assertEqual(settings.NULL_CHOICE_VALUE, 'null')
|
||||
|
||||
|
||||
class StrictnessTests(TestCase):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
def test_settings_default(self):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_ignore(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_return_no_results(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RETURN_NO_RESULTS):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_raise_validation_error(self):
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.RAISE_VALIDATION_ERROR):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
|
||||
|
||||
def test_legacy_ignore(self):
|
||||
with override_settings(FILTERS_STRICTNESS=False):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_legacy_return_no_results(self):
|
||||
with override_settings(FILTERS_STRICTNESS=True):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
def test_legacy_raise_validation_error(self):
|
||||
with override_settings(FILTERS_STRICTNESS='RAISE'):
|
||||
self.assertEqual(self.F().strict, STRICTNESS.RAISE_VALIDATION_ERROR)
|
||||
|
||||
def test_legacy_differentiation(self):
|
||||
self.assertNotEqual(STRICTNESS.IGNORE, False)
|
||||
self.assertNotEqual(STRICTNESS.RETURN_NO_RESULTS, True)
|
||||
self.assertNotEqual(STRICTNESS.RAISE_VALIDATION_ERROR, 'RAISE')
|
||||
|
||||
|
||||
class OverrideSettingsTests(TestCase):
|
||||
|
||||
def test_attribute_override(self):
|
||||
self.assertIsInstance(settings.VERBOSE_LOOKUPS, dict)
|
||||
|
||||
original = settings.VERBOSE_LOOKUPS
|
||||
|
||||
with override_settings(FILTERS_VERBOSE_LOOKUPS=None):
|
||||
self.assertIsNone(settings.VERBOSE_LOOKUPS)
|
||||
|
||||
self.assertIs(settings.VERBOSE_LOOKUPS, original)
|
||||
|
||||
def test_missing_attribute_override(self):
|
||||
# ensure that changed setting behaves correctly when
|
||||
# not originally present in the user's settings.
|
||||
from django.conf import settings as dj_settings
|
||||
self.assertFalse(hasattr(dj_settings, 'FILTERS_HELP_TEXT_FILTER'))
|
||||
|
||||
# Default value
|
||||
self.assertTrue(settings.HELP_TEXT_FILTER)
|
||||
|
||||
with override_settings(FILTERS_HELP_TEXT_FILTER=None):
|
||||
self.assertIsNone(settings.HELP_TEXT_FILTER)
|
||||
|
||||
# Revert to default
|
||||
self.assertTrue(settings.HELP_TEXT_FILTER)
|
||||
|
||||
def test_non_filters_setting(self):
|
||||
self.assertFalse(hasattr(settings, 'USE_TZ'))
|
||||
|
||||
with override_settings(USE_TZ=False):
|
||||
self.assertFalse(hasattr(settings, 'USE_TZ'))
|
||||
|
||||
self.assertFalse(hasattr(settings, 'USE_TZ'))
|
||||
|
||||
def test_non_existent_setting(self):
|
||||
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
|
||||
self.assertFalse(hasattr(settings, 'FOOBAR'))
|
||||
|
||||
with override_settings(FILTERS_FOOBAR='blah'):
|
||||
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
|
||||
self.assertFalse(hasattr(settings, 'FOOBAR'))
|
||||
|
||||
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
|
||||
self.assertFalse(hasattr(settings, 'FOOBAR'))
|
||||
|
||||
|
||||
class IsCallableTests(TestCase):
|
||||
|
||||
def test_behavior(self):
|
||||
def func():
|
||||
pass
|
||||
|
||||
class Class(object):
|
||||
def __call__(self):
|
||||
pass
|
||||
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
c = Class()
|
||||
|
||||
self.assertTrue(is_callable(func))
|
||||
self.assertFalse(is_callable(Class))
|
||||
self.assertTrue(is_callable(c))
|
||||
self.assertTrue(is_callable(c.method))
|
|
@ -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))
|
|
@ -0,0 +1,272 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import decimal
|
||||
from datetime import datetime, time, timedelta, tzinfo
|
||||
|
||||
import pytz
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from django_filters.fields import (
|
||||
BaseCSVField,
|
||||
BaseRangeField,
|
||||
DateRangeField,
|
||||
DateTimeRangeField,
|
||||
IsoDateTimeField,
|
||||
Lookup,
|
||||
LookupTypeField,
|
||||
RangeField,
|
||||
TimeRangeField
|
||||
)
|
||||
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
|
||||
|
||||
|
||||
def to_d(float_value):
|
||||
return decimal.Decimal('%.2f' % float_value)
|
||||
|
||||
|
||||
class LookupBoolTests(TestCase):
|
||||
def test_lookup_true(self):
|
||||
self.assertTrue(Lookup(True, 'exact'))
|
||||
self.assertTrue(Lookup(1, 'exact'))
|
||||
self.assertTrue(Lookup('1', 'exact'))
|
||||
self.assertTrue(Lookup(datetime.now(), 'exact'))
|
||||
|
||||
def test_lookup_false(self):
|
||||
self.assertFalse(Lookup(False, 'exact'))
|
||||
self.assertFalse(Lookup(0, 'exact'))
|
||||
self.assertFalse(Lookup('', 'exact'))
|
||||
self.assertFalse(Lookup(None, 'exact'))
|
||||
|
||||
|
||||
class RangeFieldTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
f = RangeField()
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = RangeField(widget=w, required=False)
|
||||
|
||||
self.assertEqual(
|
||||
f.clean(['12.34', '55']),
|
||||
slice(to_d(12.34), to_d(55)))
|
||||
self.assertIsNone(f.clean([]))
|
||||
|
||||
|
||||
class DateRangeFieldTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
f = DateRangeField()
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
|
||||
@override_settings(USE_TZ=False)
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = DateRangeField(widget=w, required=False)
|
||||
self.assertEqual(
|
||||
f.clean(['2015-01-01', '2015-01-10']),
|
||||
slice(datetime(2015, 1, 1, 0, 0, 0),
|
||||
datetime(2015, 1, 10, 23, 59, 59, 999999)))
|
||||
self.assertIsNone(f.clean([]))
|
||||
|
||||
|
||||
class DateTimeRangeFieldTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
f = DateTimeRangeField()
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
|
||||
@override_settings(USE_TZ=False)
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = DateTimeRangeField(widget=w)
|
||||
self.assertEqual(
|
||||
f.clean(['2015-01-01 10:30', '2015-01-10 8:45']),
|
||||
slice(datetime(2015, 1, 1, 10, 30, 0),
|
||||
datetime(2015, 1, 10, 8, 45, 0)))
|
||||
|
||||
|
||||
class TimeRangeFieldTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
f = DateRangeField()
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
|
||||
def test_clean(self):
|
||||
w = RangeWidget()
|
||||
f = TimeRangeField(widget=w)
|
||||
|
||||
self.assertEqual(
|
||||
f.clean(['10:15', '12:30']),
|
||||
slice(time(10, 15, 0), time(12, 30, 0)))
|
||||
|
||||
|
||||
class LookupTypeFieldTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
inner = forms.DecimalField()
|
||||
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
|
||||
def test_clean(self):
|
||||
inner = forms.DecimalField()
|
||||
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')], required=False)
|
||||
self.assertEqual(
|
||||
f.clean(['12.34', 'lt']),
|
||||
Lookup(to_d(12.34), 'lt'))
|
||||
self.assertEqual(
|
||||
f.clean([]),
|
||||
Lookup(value=None, lookup_type='exact'))
|
||||
|
||||
def test_render_used_html5(self):
|
||||
inner = forms.DecimalField()
|
||||
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
|
||||
self.assertHTMLEqual(f.widget.render('price', ''), """
|
||||
<input type="number" step="any" name="price_0" />
|
||||
<select name="price_1">
|
||||
<option value="gt">gt</option>
|
||||
<option value="lt">lt</option>
|
||||
</select>""")
|
||||
self.assertHTMLEqual(f.widget.render('price', ['abc', 'lt']), """
|
||||
<input type="number" step="any" name="price_0" value="abc" />
|
||||
<select name="price_1">
|
||||
<option value="gt">gt</option>
|
||||
<option selected="selected" value="lt">lt</option>
|
||||
</select>""")
|
||||
|
||||
|
||||
class IsoDateTimeFieldTests(TestCase):
|
||||
reference_str = "2015-07-19T13:34:51.759"
|
||||
reference_dt = datetime(2015, 7, 19, 13, 34, 51, 759000)
|
||||
field = IsoDateTimeField()
|
||||
|
||||
def parse_input(self, value):
|
||||
return self.field.strptime(value, IsoDateTimeField.ISO_8601)
|
||||
|
||||
def test_datetime_string_is_parsed(self):
|
||||
d = self.parse_input(self.reference_str)
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_string_with_timezone_is_parsed(self):
|
||||
d = self.parse_input(self.reference_str + "+01:00")
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
def test_datetime_zulu(self):
|
||||
d = self.parse_input(self.reference_str + "Z")
|
||||
self.assertTrue(isinstance(d, datetime))
|
||||
|
||||
@override_settings(TIME_ZONE='UTC')
|
||||
def test_datetime_timezone_awareness(self):
|
||||
utc, tokyo = pytz.timezone('UTC'), pytz.timezone('Asia/Tokyo')
|
||||
|
||||
# by default, use the server timezone
|
||||
reference = utc.localize(self.reference_dt)
|
||||
parsed = self.parse_input(self.reference_str)
|
||||
self.assertIsInstance(parsed.tzinfo, tzinfo)
|
||||
self.assertEqual(parsed, reference)
|
||||
|
||||
# if set, use the active timezone
|
||||
reference = tokyo.localize(self.reference_dt)
|
||||
with timezone.override(tokyo):
|
||||
parsed = self.parse_input(self.reference_str)
|
||||
self.assertIsInstance(parsed.tzinfo, tzinfo)
|
||||
self.assertEqual(parsed.tzinfo.zone, tokyo.zone)
|
||||
self.assertEqual(parsed, reference)
|
||||
|
||||
# if provided, utc offset should have precedence
|
||||
reference = utc.localize(self.reference_dt - timedelta(hours=1))
|
||||
parsed = self.parse_input(self.reference_str + "+01:00")
|
||||
self.assertIsInstance(parsed.tzinfo, tzinfo)
|
||||
self.assertEqual(parsed, reference)
|
||||
|
||||
@override_settings(USE_TZ=False)
|
||||
def test_datetime_timezone_naivety(self):
|
||||
reference = self.reference_dt.replace()
|
||||
|
||||
parsed = self.parse_input(self.reference_str + "+01:00")
|
||||
self.assertIsNone(parsed.tzinfo)
|
||||
self.assertEqual(parsed, reference - timedelta(hours=1))
|
||||
|
||||
parsed = self.parse_input(self.reference_str)
|
||||
self.assertIsNone(parsed.tzinfo)
|
||||
self.assertEqual(parsed, reference)
|
||||
|
||||
def test_datetime_non_iso_format(self):
|
||||
f = IsoDateTimeField()
|
||||
parsed = f.strptime('19-07-2015T51:34:13.759', '%d-%m-%YT%S:%M:%H.%f')
|
||||
self.assertTrue(isinstance(parsed, datetime))
|
||||
self.assertEqual(parsed, self.reference_dt)
|
||||
|
||||
def test_datetime_wrong_format(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.parse_input('19-07-2015T51:34:13.759')
|
||||
|
||||
|
||||
class BaseCSVFieldTests(TestCase):
|
||||
def setUp(self):
|
||||
class DecimalCSVField(BaseCSVField, forms.DecimalField):
|
||||
pass
|
||||
|
||||
self.field = DecimalCSVField()
|
||||
|
||||
def test_clean(self):
|
||||
self.assertEqual(self.field.clean(None), None)
|
||||
self.assertEqual(self.field.clean(''), [])
|
||||
self.assertEqual(self.field.clean(['1']), [1])
|
||||
self.assertEqual(self.field.clean(['1', '2']), [1, 2])
|
||||
self.assertEqual(self.field.clean(['1', '2', '3']), [1, 2, 3])
|
||||
|
||||
def test_validation_error(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean([''])
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean(['a', 'b', 'c'])
|
||||
|
||||
def test_derived_widget(self):
|
||||
with self.assertRaises(AssertionError) as excinfo:
|
||||
BaseCSVField(widget=RangeWidget())
|
||||
|
||||
msg = str(excinfo.exception)
|
||||
self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
|
||||
self.assertIn("RangeWidget", msg)
|
||||
|
||||
widget = CSVWidget(attrs={'class': 'class'})
|
||||
field = BaseCSVField(widget=widget)
|
||||
self.assertIsInstance(field.widget, CSVWidget)
|
||||
self.assertEqual(field.widget.attrs, {'class': 'class'})
|
||||
|
||||
field = BaseCSVField(widget=CSVWidget)
|
||||
self.assertIsInstance(field.widget, CSVWidget)
|
||||
|
||||
field = BaseCSVField(widget=forms.Select)
|
||||
self.assertIsInstance(field.widget, forms.Select)
|
||||
self.assertIsInstance(field.widget, BaseCSVWidget)
|
||||
|
||||
|
||||
class BaseRangeFieldTests(TestCase):
|
||||
def setUp(self):
|
||||
class DecimalRangeField(BaseRangeField, forms.DecimalField):
|
||||
pass
|
||||
|
||||
self.field = DecimalRangeField()
|
||||
|
||||
def test_clean(self):
|
||||
self.assertEqual(self.field.clean(None), None)
|
||||
self.assertEqual(self.field.clean(['1', '2']), [1, 2])
|
||||
|
||||
def test_validation_error(self):
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean('')
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean([''])
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean(['1'])
|
||||
|
||||
with self.assertRaises(forms.ValidationError):
|
||||
self.field.clean(['1', '2', '3'])
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,890 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import mock
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.constants import STRICTNESS
|
||||
from django_filters.filters import (
|
||||
BaseInFilter,
|
||||
BaseRangeFilter,
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
ChoiceFilter,
|
||||
DateRangeFilter,
|
||||
Filter,
|
||||
FilterMethod,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
NumberFilter,
|
||||
UUIDFilter
|
||||
)
|
||||
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS, FilterSet
|
||||
from django_filters.widgets import BooleanWidget
|
||||
|
||||
from .models import (
|
||||
Account,
|
||||
AdminUser,
|
||||
Article,
|
||||
BankAccount,
|
||||
Book,
|
||||
Business,
|
||||
Comment,
|
||||
DirectedNode,
|
||||
NetworkSetting,
|
||||
Node,
|
||||
Profile,
|
||||
Restaurant,
|
||||
SubnetMaskField,
|
||||
User,
|
||||
UUIDTestModel,
|
||||
Worker
|
||||
)
|
||||
|
||||
|
||||
def checkItemsEqual(L1, L2):
|
||||
"""
|
||||
TestCase.assertItemsEqual() is not available in Python 2.6.
|
||||
"""
|
||||
return len(L1) == len(L2) and sorted(L1) == sorted(L2)
|
||||
|
||||
|
||||
class HelperMethodsTests(TestCase):
|
||||
|
||||
@unittest.skip('todo')
|
||||
def test_get_declared_filters(self):
|
||||
pass
|
||||
|
||||
@unittest.skip('todo')
|
||||
def test_filters_for_model(self):
|
||||
pass
|
||||
|
||||
@unittest.skip('todo')
|
||||
def test_filterset_factory(self):
|
||||
pass
|
||||
|
||||
|
||||
class DbFieldDefaultFiltersTests(TestCase):
|
||||
|
||||
def test_expected_db_fields_get_filters(self):
|
||||
to_check = [
|
||||
models.BooleanField,
|
||||
models.CharField,
|
||||
models.CommaSeparatedIntegerField,
|
||||
models.DateField,
|
||||
models.DateTimeField,
|
||||
models.DecimalField,
|
||||
models.EmailField,
|
||||
models.FilePathField,
|
||||
models.FloatField,
|
||||
models.IntegerField,
|
||||
models.GenericIPAddressField,
|
||||
models.NullBooleanField,
|
||||
models.PositiveIntegerField,
|
||||
models.PositiveSmallIntegerField,
|
||||
models.SlugField,
|
||||
models.SmallIntegerField,
|
||||
models.TextField,
|
||||
models.TimeField,
|
||||
models.DurationField,
|
||||
models.URLField,
|
||||
models.ForeignKey,
|
||||
models.OneToOneField,
|
||||
models.ManyToManyField,
|
||||
models.UUIDField,
|
||||
]
|
||||
msg = "%s expected to be found in FILTER_FOR_DBFIELD_DEFAULTS"
|
||||
|
||||
for m in to_check:
|
||||
self.assertIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__)
|
||||
|
||||
def test_expected_db_fields_do_not_get_filters(self):
|
||||
to_check = [
|
||||
models.Field,
|
||||
models.BigIntegerField,
|
||||
models.FileField,
|
||||
models.ImageField,
|
||||
]
|
||||
msg = "%s expected to not be found in FILTER_FOR_DBFIELD_DEFAULTS"
|
||||
|
||||
for m in to_check:
|
||||
self.assertNotIn(m, FILTER_FOR_DBFIELD_DEFAULTS, msg % m.__name__)
|
||||
|
||||
|
||||
class FilterSetFilterForFieldTests(TestCase):
|
||||
|
||||
def test_filter_found_for_field(self):
|
||||
f = User._meta.get_field('username')
|
||||
result = FilterSet.filter_for_field(f, 'username')
|
||||
self.assertIsInstance(result, CharFilter)
|
||||
self.assertEqual(result.name, 'username')
|
||||
|
||||
def test_filter_found_for_uuidfield(self):
|
||||
f = UUIDTestModel._meta.get_field('uuid')
|
||||
result = FilterSet.filter_for_field(f, 'uuid')
|
||||
self.assertIsInstance(result, UUIDFilter)
|
||||
self.assertEqual(result.name, 'uuid')
|
||||
|
||||
def test_filter_found_for_autofield(self):
|
||||
f = User._meta.get_field('id')
|
||||
result = FilterSet.filter_for_field(f, 'id')
|
||||
self.assertIsInstance(result, NumberFilter)
|
||||
self.assertEqual(result.name, 'id')
|
||||
|
||||
def test_field_with_extras(self):
|
||||
f = User._meta.get_field('favorite_books')
|
||||
result = FilterSet.filter_for_field(f, 'favorite_books')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'favorite_books')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Book)
|
||||
|
||||
def test_field_with_choices(self):
|
||||
f = User._meta.get_field('status')
|
||||
result = FilterSet.filter_for_field(f, 'status')
|
||||
self.assertIsInstance(result, ChoiceFilter)
|
||||
self.assertEqual(result.name, 'status')
|
||||
self.assertTrue('choices' in result.extra)
|
||||
self.assertIsNotNone(result.extra['choices'])
|
||||
|
||||
def test_field_that_is_subclassed(self):
|
||||
f = User._meta.get_field('first_name')
|
||||
result = FilterSet.filter_for_field(f, 'first_name')
|
||||
self.assertIsInstance(result, CharFilter)
|
||||
|
||||
def test_unknown_field_type_error(self):
|
||||
f = NetworkSetting._meta.get_field('mask')
|
||||
|
||||
with self.assertRaises(AssertionError) as excinfo:
|
||||
FilterSet.filter_for_field(f, 'mask')
|
||||
|
||||
self.assertIn(
|
||||
"FilterSet resolved field 'mask' with 'exact' lookup "
|
||||
"to an unrecognized field type SubnetMaskField",
|
||||
excinfo.exception.args[0])
|
||||
|
||||
def test_symmetrical_selfref_m2m_field(self):
|
||||
f = Node._meta.get_field('adjacents')
|
||||
result = FilterSet.filter_for_field(f, 'adjacents')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'adjacents')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Node)
|
||||
|
||||
def test_non_symmetrical_selfref_m2m_field(self):
|
||||
f = DirectedNode._meta.get_field('outbound_nodes')
|
||||
result = FilterSet.filter_for_field(f, 'outbound_nodes')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'outbound_nodes')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, DirectedNode)
|
||||
|
||||
def test_m2m_field_with_through_model(self):
|
||||
f = Business._meta.get_field('employees')
|
||||
result = FilterSet.filter_for_field(f, 'employees')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'employees')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Worker)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||
def test_transformed_lookup_expr(self):
|
||||
f = Comment._meta.get_field('date')
|
||||
result = FilterSet.filter_for_field(f, 'date', 'year__gte')
|
||||
self.assertIsInstance(result, NumberFilter)
|
||||
self.assertEqual(result.name, 'date')
|
||||
|
||||
@unittest.skip('todo')
|
||||
def test_filter_overrides(self):
|
||||
pass
|
||||
|
||||
|
||||
class FilterSetFilterForLookupTests(TestCase):
|
||||
|
||||
def test_filter_for_ISNULL_lookup(self):
|
||||
f = Article._meta.get_field('author')
|
||||
result, params = FilterSet.filter_for_lookup(f, 'isnull')
|
||||
self.assertEqual(result, BooleanFilter)
|
||||
self.assertDictEqual(params, {})
|
||||
|
||||
def test_filter_for_IN_lookup(self):
|
||||
f = Article._meta.get_field('author')
|
||||
result, params = FilterSet.filter_for_lookup(f, 'in')
|
||||
self.assertTrue(issubclass(result, ModelChoiceFilter))
|
||||
self.assertTrue(issubclass(result, BaseInFilter))
|
||||
self.assertEqual(params['to_field_name'], 'id')
|
||||
|
||||
def test_filter_for_RANGE_lookup(self):
|
||||
f = Article._meta.get_field('author')
|
||||
result, params = FilterSet.filter_for_lookup(f, 'range')
|
||||
self.assertTrue(issubclass(result, ModelChoiceFilter))
|
||||
self.assertTrue(issubclass(result, BaseRangeFilter))
|
||||
self.assertEqual(params['to_field_name'], 'id')
|
||||
|
||||
def test_isnull_with_filter_overrides(self):
|
||||
class OFilterSet(FilterSet):
|
||||
class Meta:
|
||||
filter_overrides = {
|
||||
models.BooleanField: {
|
||||
'filter_class': BooleanFilter,
|
||||
'extra': lambda f: {
|
||||
'widget': BooleanWidget,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
f = Article._meta.get_field('author')
|
||||
result, params = OFilterSet.filter_for_lookup(f, 'isnull')
|
||||
self.assertEqual(result, BooleanFilter)
|
||||
self.assertEqual(params['widget'], BooleanWidget)
|
||||
|
||||
|
||||
class FilterSetFilterForReverseFieldTests(TestCase):
|
||||
|
||||
def test_reverse_o2o_relationship(self):
|
||||
f = Account._meta.get_field('profile')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'profile')
|
||||
self.assertIsInstance(result, ModelChoiceFilter)
|
||||
self.assertEqual(result.name, 'profile')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Profile)
|
||||
|
||||
def test_reverse_fk_relationship(self):
|
||||
f = User._meta.get_field('comments')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'comments')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'comments')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Comment)
|
||||
|
||||
def test_reverse_m2m_relationship(self):
|
||||
f = Book._meta.get_field('lovers')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'lovers')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'lovers')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, User)
|
||||
|
||||
def test_reverse_non_symmetrical_selfref_m2m_field(self):
|
||||
f = DirectedNode._meta.get_field('inbound_nodes')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'inbound_nodes')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'inbound_nodes')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, DirectedNode)
|
||||
|
||||
def test_reverse_m2m_field_with_through_model(self):
|
||||
f = Worker._meta.get_field('employers')
|
||||
result = FilterSet.filter_for_reverse_field(f, 'employers')
|
||||
self.assertIsInstance(result, ModelMultipleChoiceFilter)
|
||||
self.assertEqual(result.name, 'employers')
|
||||
self.assertTrue('queryset' in result.extra)
|
||||
self.assertIsNotNone(result.extra['queryset'])
|
||||
self.assertEqual(result.extra['queryset'].model, Business)
|
||||
|
||||
|
||||
class FilterSetClassCreationTests(TestCase):
|
||||
|
||||
def test_no_filters(self):
|
||||
class F(FilterSet):
|
||||
pass
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 0)
|
||||
|
||||
def test_declaring_filter(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertListEqual(list(F.declared_filters), ['username'])
|
||||
self.assertEqual(len(F.base_filters), 1)
|
||||
self.assertListEqual(list(F.base_filters), ['username'])
|
||||
|
||||
def test_model_derived(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = '__all__'
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'price', 'average_rating'])
|
||||
|
||||
def test_model_no_fields_or_exclude(self):
|
||||
with self.assertRaises(AssertionError) as excinfo:
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
|
||||
self.assertIn(
|
||||
"Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude'",
|
||||
str(excinfo.exception)
|
||||
)
|
||||
|
||||
def test_model_fields_empty(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = []
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 0)
|
||||
self.assertListEqual(list(F.base_filters), [])
|
||||
|
||||
def test_model_exclude_empty(self):
|
||||
# equivalent to fields = '__all__'
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
exclude = []
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'price', 'average_rating'])
|
||||
|
||||
def test_declared_and_model_derived(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = '__all__'
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 4)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'price', 'average_rating', 'username'])
|
||||
|
||||
def test_meta_fields_with_declared_and_model_derived(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('username', 'price')
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 2)
|
||||
self.assertListEqual(list(F.base_filters), ['username', 'price'])
|
||||
|
||||
def test_meta_fields_dictionary_derived(self):
|
||||
class F(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = {'price': ['exact', 'gte', 'lte'], }
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
|
||||
expected_list = ['price', 'price__gte', 'price__lte', ]
|
||||
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
|
||||
|
||||
def test_meta_fields_containing_autofield(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('id', 'username', 'price')
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
self.assertListEqual(list(F.base_filters), ['id', 'username', 'price'])
|
||||
|
||||
def test_meta_fields_dictionary_autofield(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = {'id': ['exact'],
|
||||
'username': ['exact'],
|
||||
}
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 2)
|
||||
|
||||
expected_list = ['id', 'username']
|
||||
self.assertTrue(checkItemsEqual(list(F.base_filters), expected_list))
|
||||
|
||||
def test_meta_fields_containing_unknown(self):
|
||||
with self.assertRaises(TypeError) as excinfo:
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('username', 'price', 'other', 'another')
|
||||
|
||||
self.assertEqual(
|
||||
str(excinfo.exception),
|
||||
"'Meta.fields' contains fields that are not defined on this FilterSet: "
|
||||
"other, another"
|
||||
)
|
||||
|
||||
def test_meta_fields_dictionary_containing_unknown(self):
|
||||
with self.assertRaises(TypeError):
|
||||
class F(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = {'id': ['exact'],
|
||||
'title': ['exact'],
|
||||
'other': ['exact'],
|
||||
}
|
||||
|
||||
def test_meta_exlude_with_declared_and_declared_wins(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
exclude = ('username', 'price')
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 3)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'average_rating', 'username'])
|
||||
|
||||
def test_meta_fields_and_exlude_and_exclude_wins(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('username', 'title', 'price')
|
||||
exclude = ('title',)
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 1)
|
||||
self.assertEqual(len(F.base_filters), 2)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['username', 'price'])
|
||||
|
||||
def test_meta_exlude_with_no_fields(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
exclude = ('price', )
|
||||
|
||||
self.assertEqual(len(F.declared_filters), 0)
|
||||
self.assertEqual(len(F.base_filters), 2)
|
||||
self.assertListEqual(list(F.base_filters),
|
||||
['title', 'average_rating'])
|
||||
|
||||
def test_filterset_class_inheritance(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = '__all__'
|
||||
|
||||
class G(F):
|
||||
pass
|
||||
self.assertEqual(set(F.base_filters), set(G.base_filters))
|
||||
|
||||
class F(FilterSet):
|
||||
other = CharFilter
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = '__all__'
|
||||
|
||||
class G(F):
|
||||
pass
|
||||
self.assertEqual(set(F.base_filters), set(G.base_filters))
|
||||
|
||||
def test_abstract_model_inheritance(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Restaurant
|
||||
fields = '__all__'
|
||||
|
||||
self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza']))
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Restaurant
|
||||
fields = ['name', 'serves_pizza']
|
||||
|
||||
self.assertEqual(set(F.base_filters), set(['name', 'serves_pizza']))
|
||||
|
||||
def test_custom_field_gets_filter_from_override(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = NetworkSetting
|
||||
fields = '__all__'
|
||||
|
||||
filter_overrides = {
|
||||
SubnetMaskField: {'filter_class': CharFilter}
|
||||
}
|
||||
|
||||
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr'])
|
||||
|
||||
def test_custom_declared_field_no_warning(self):
|
||||
class F(FilterSet):
|
||||
mask = CharFilter()
|
||||
|
||||
class Meta:
|
||||
model = NetworkSetting
|
||||
fields = ['mask']
|
||||
|
||||
self.assertEqual(list(F.base_filters.keys()), ['mask'])
|
||||
|
||||
def test_filterset_for_proxy_model(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = '__all__'
|
||||
|
||||
class ProxyF(FilterSet):
|
||||
class Meta:
|
||||
model = AdminUser
|
||||
fields = '__all__'
|
||||
|
||||
self.assertEqual(list(F.base_filters), list(ProxyF.base_filters))
|
||||
|
||||
def test_filterset_for_mti_model(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = '__all__'
|
||||
|
||||
class FtiF(FilterSet):
|
||||
class Meta:
|
||||
model = BankAccount
|
||||
fields = '__all__'
|
||||
|
||||
# fails due to 'account_ptr' getting picked up
|
||||
self.assertEqual(
|
||||
list(F.base_filters) + ['amount_saved'],
|
||||
list(FtiF.base_filters))
|
||||
|
||||
def test_declared_filter_disabling(self):
|
||||
class Parent(FilterSet):
|
||||
f1 = CharFilter()
|
||||
f2 = CharFilter()
|
||||
|
||||
class Child(Parent):
|
||||
f1 = None
|
||||
|
||||
class Grandchild(Child):
|
||||
pass
|
||||
|
||||
self.assertEqual(len(Parent.base_filters), 2)
|
||||
self.assertEqual(len(Child.base_filters), 1)
|
||||
self.assertEqual(len(Grandchild.base_filters), 1)
|
||||
|
||||
|
||||
class FilterSetInstantiationTests(TestCase):
|
||||
|
||||
def test_creating_instance(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
f = F()
|
||||
self.assertFalse(f.is_bound)
|
||||
self.assertIsNotNone(f.queryset)
|
||||
self.assertEqual(len(f.filters), len(F.base_filters))
|
||||
for name, filter_ in f.filters.items():
|
||||
self.assertEqual(
|
||||
filter_.model,
|
||||
User,
|
||||
"%s does not have model set correctly" % name)
|
||||
|
||||
def test_creating_bound_instance(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
f = F({'username': 'username'})
|
||||
self.assertTrue(f.is_bound)
|
||||
|
||||
def test_creating_with_queryset(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
m = mock.Mock()
|
||||
f = F(queryset=m)
|
||||
self.assertEqual(f.queryset, m)
|
||||
|
||||
def test_creating_with_request(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
m = mock.Mock()
|
||||
f = F(request=m)
|
||||
self.assertEqual(f.request, m)
|
||||
|
||||
|
||||
class FilterSetStrictnessTests(TestCase):
|
||||
|
||||
def test_settings_default(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
# Ensure default is not IGNORE
|
||||
self.assertEqual(F().strict, STRICTNESS.RETURN_NO_RESULTS)
|
||||
|
||||
# override and test
|
||||
with override_settings(FILTERS_STRICTNESS=STRICTNESS.IGNORE):
|
||||
self.assertEqual(F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_meta_value(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
strict = STRICTNESS.IGNORE
|
||||
|
||||
self.assertEqual(F().strict, STRICTNESS.IGNORE)
|
||||
|
||||
def test_init_default(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
strict = STRICTNESS.IGNORE
|
||||
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
self.assertEqual(F(strict=strict).strict, strict)
|
||||
|
||||
def test_legacy_value(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
self.assertEqual(F(strict=False).strict, STRICTNESS.IGNORE)
|
||||
|
||||
|
||||
class FilterSetTogetherTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.alex = User.objects.create(username='alex', status=1)
|
||||
self.jacob = User.objects.create(username='jacob', status=2)
|
||||
self.qs = User.objects.all().order_by('id')
|
||||
|
||||
def test_fields_set(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'status', 'is_active', 'first_name']
|
||||
together = [
|
||||
('username', 'status'),
|
||||
('first_name', 'is_active'),
|
||||
]
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
|
||||
f = F({}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 2)
|
||||
|
||||
f = F({'username': 'alex'}, queryset=self.qs)
|
||||
with self.assertRaises(ValidationError):
|
||||
f.qs.count()
|
||||
|
||||
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 1)
|
||||
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
|
||||
|
||||
def test_single_fields_set(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'status']
|
||||
together = ['username', 'status']
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
|
||||
f = F({}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 2)
|
||||
|
||||
f = F({'username': 'alex'}, queryset=self.qs)
|
||||
with self.assertRaises(ValidationError):
|
||||
f.qs.count()
|
||||
|
||||
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 1)
|
||||
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
|
||||
|
||||
def test_empty_values(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'status']
|
||||
together = ['username', 'status']
|
||||
|
||||
f = F({'username': '', 'status': ''}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 2)
|
||||
f = F({'username': 'alex', 'status': ''}, queryset=self.qs)
|
||||
self.assertEqual(f.qs.count(), 0)
|
||||
|
||||
|
||||
# test filter.method here, as it depends on its parent FilterSet
|
||||
class FilterMethodTests(TestCase):
|
||||
|
||||
def test_none(self):
|
||||
# use a mock to bypass bound/unbound method equality
|
||||
class TestFilter(Filter):
|
||||
filter = mock.Mock()
|
||||
|
||||
f = TestFilter(method=None)
|
||||
self.assertIsNone(f.method)
|
||||
|
||||
# passing method=None should not modify filter function
|
||||
self.assertIs(f.filter, TestFilter.filter)
|
||||
|
||||
def test_method_name(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
|
||||
def filter_f(self, qs, name, value):
|
||||
pass
|
||||
|
||||
f = F({}, queryset=User.objects.all())
|
||||
self.assertEqual(f.filters['f'].method, 'filter_f')
|
||||
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
|
||||
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
|
||||
|
||||
def test_method_callable(self):
|
||||
def filter_f(qs, name, value):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
f = Filter(method=filter_f)
|
||||
|
||||
f = F({}, queryset=User.objects.all())
|
||||
self.assertEqual(f.filters['f'].method, filter_f)
|
||||
self.assertEqual(f.filters['f'].filter.method, filter_f)
|
||||
self.assertIsInstance(f.filters['f'].filter, FilterMethod)
|
||||
|
||||
def test_request_available_during_method_called(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
|
||||
def filter_f(self, qs, name, value):
|
||||
# call mock request object to prove self.request can be accessed
|
||||
self.request()
|
||||
|
||||
m = mock.Mock()
|
||||
f = F({}, queryset=User.objects.all(), request=m)
|
||||
# call the filter
|
||||
f.filters['f'].filter.method(User.objects.all(), 'f', '')
|
||||
m.assert_called_once_with()
|
||||
|
||||
def test_method_with_overridden_filter(self):
|
||||
# Some filter classes override the base filter() method. We need
|
||||
# to ensure that passing a method argument still works correctly
|
||||
class F(FilterSet):
|
||||
f = DateRangeFilter(method='filter_f')
|
||||
|
||||
def filter_f(self, qs, name, value):
|
||||
pass
|
||||
|
||||
f = F({}, queryset=User.objects.all())
|
||||
self.assertEqual(f.filters['f'].method, 'filter_f')
|
||||
self.assertEqual(f.filters['f'].filter.method, f.filter_f)
|
||||
|
||||
def test_parent_unresolvable(self):
|
||||
f = Filter(method='filter_f')
|
||||
with self.assertRaises(AssertionError) as w:
|
||||
f.filter(User.objects.all(), 0)
|
||||
|
||||
self.assertIn("'None'", str(w.exception))
|
||||
self.assertIn('parent', str(w.exception))
|
||||
self.assertIn('filter_f', str(w.exception))
|
||||
|
||||
def test_method_self_is_parent(self):
|
||||
# Ensure the method isn't 're-parented' on the `FilterMethod` helper class.
|
||||
# Filter methods should have access to the filterset's properties.
|
||||
request = mock.Mock()
|
||||
|
||||
class F(FilterSet):
|
||||
f = CharFilter(method='filter_f')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
||||
|
||||
def filter_f(inner_self, qs, name, value):
|
||||
self.assertIsInstance(inner_self, F)
|
||||
self.assertIs(inner_self.request, request)
|
||||
|
||||
F({'f': 'foo'}, request=request, queryset=User.objects.all()).qs
|
||||
|
||||
def test_method_unresolvable(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
|
||||
f = F({}, queryset=User.objects.all())
|
||||
|
||||
with self.assertRaises(AssertionError) as w:
|
||||
f.filters['f'].filter(User.objects.all(), 0)
|
||||
|
||||
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
|
||||
self.assertIn('.filter_f()', str(w.exception))
|
||||
|
||||
def test_method_uncallable(self):
|
||||
class F(FilterSet):
|
||||
f = Filter(method='filter_f')
|
||||
filter_f = 4
|
||||
|
||||
f = F({}, queryset=User.objects.all())
|
||||
|
||||
with self.assertRaises(AssertionError) as w:
|
||||
f.filters['f'].filter(User.objects.all(), 0)
|
||||
|
||||
self.assertIn('%s.%s' % (F.__module__, F.__name__), str(w.exception))
|
||||
self.assertIn('.filter_f()', str(w.exception))
|
||||
|
||||
def test_method_set_unset(self):
|
||||
# use a mock to bypass bound/unbound method equality
|
||||
class TestFilter(Filter):
|
||||
filter = mock.Mock()
|
||||
|
||||
f = TestFilter(method='filter_f')
|
||||
self.assertEqual(f.method, 'filter_f')
|
||||
self.assertIsInstance(f.filter, FilterMethod)
|
||||
|
||||
# setting None should revert to Filter.filter
|
||||
f.method = None
|
||||
self.assertIsNone(f.method)
|
||||
self.assertIs(f.filter, TestFilter.filter)
|
||||
|
||||
|
||||
class MiscFilterSetTests(TestCase):
|
||||
|
||||
def test_no__getitem__(self):
|
||||
# The DTL processes variable lookups by the following rules:
|
||||
# https://docs.djangoproject.com/en/1.9/ref/templates/language/#variables
|
||||
# A __getitem__ implementation precedes normal attribute access, and in
|
||||
# the case of #58, will force the queryset to evaluate when it should
|
||||
# not (eg, when rendering a blank form).
|
||||
self.assertFalse(hasattr(FilterSet, '__getitem__'))
|
||||
|
||||
def test_no_qs_proxying(self):
|
||||
# The FilterSet should not proxy .qs methods - just access .qs directly
|
||||
self.assertFalse(hasattr(FilterSet, '__len__'))
|
||||
self.assertFalse(hasattr(FilterSet, '__iter__'))
|
|
@ -0,0 +1,227 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from django_filters.filters import CharFilter, ChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
|
||||
from .models import MANAGER, REGULAR, STATUS_CHOICES, Book, ManagerGroup, User
|
||||
|
||||
|
||||
class FilterSetFormTests(TestCase):
|
||||
|
||||
def test_form_from_empty_filterset(self):
|
||||
class F(FilterSet):
|
||||
pass
|
||||
|
||||
f = F(queryset=Book.objects.all()).form
|
||||
self.assertIsInstance(f, forms.Form)
|
||||
|
||||
def test_form(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertIsInstance(f, forms.Form)
|
||||
self.assertEqual(list(f.fields), ['title'])
|
||||
|
||||
def test_custom_form(self):
|
||||
class MyForm(forms.Form):
|
||||
pass
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = '__all__'
|
||||
form = MyForm
|
||||
|
||||
f = F().form
|
||||
self.assertIsInstance(f, MyForm)
|
||||
|
||||
def test_form_prefix(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertIsNone(f.prefix)
|
||||
|
||||
f = F(prefix='prefix').form
|
||||
self.assertEqual(f.prefix, 'prefix')
|
||||
|
||||
def test_form_fields(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status']
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(len(f.fields), 1)
|
||||
self.assertIn('status', f.fields)
|
||||
self.assertSequenceEqual(
|
||||
list(f.fields['status'].choices),
|
||||
(('', '---------'), ) + STATUS_CHOICES
|
||||
)
|
||||
|
||||
def test_form_fields_exclusion(self):
|
||||
class F(FilterSet):
|
||||
title = CharFilter(exclude=True)
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['title'].label, "Exclude title")
|
||||
|
||||
def test_complex_form_fields(self):
|
||||
class F(FilterSet):
|
||||
username = CharFilter(label='Filter for users with username')
|
||||
exclude_username = CharFilter(name='username', lookup_expr='iexact', exclude=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
'status': ['exact', 'lt', 'gt'],
|
||||
'favorite_books__title': ['iexact', 'in'],
|
||||
'manager_of__users__username': ['exact'],
|
||||
}
|
||||
|
||||
fields = F().form.fields
|
||||
self.assertEqual(fields['username'].label, 'Filter for users with username')
|
||||
self.assertEqual(fields['exclude_username'].label, 'Exclude username')
|
||||
self.assertEqual(fields['status'].label, 'Status')
|
||||
self.assertEqual(fields['status__lt'].label, 'Status is less than')
|
||||
self.assertEqual(fields['status__gt'].label, 'Status is greater than')
|
||||
self.assertEqual(fields['favorite_books__title__iexact'].label, 'Favorite books title')
|
||||
self.assertEqual(fields['favorite_books__title__in'].label, 'Favorite books title is in')
|
||||
self.assertEqual(fields['manager_of__users__username'].label, 'Manager of users username')
|
||||
|
||||
def test_form_fields_using_widget(self):
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(widget=forms.RadioSelect,
|
||||
choices=STATUS_CHOICES,
|
||||
empty_label=None)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status', 'username']
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(len(f.fields), 2)
|
||||
self.assertIn('status', f.fields)
|
||||
self.assertIn('username', f.fields)
|
||||
self.assertSequenceEqual(
|
||||
list(f.fields['status'].choices),
|
||||
STATUS_CHOICES
|
||||
)
|
||||
self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect)
|
||||
|
||||
def test_form_field_with_custom_label(self):
|
||||
class F(FilterSet):
|
||||
title = CharFilter(label="Book title")
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['title'].label, "Book title")
|
||||
self.assertEqual(f['title'].label, 'Book title')
|
||||
|
||||
def test_form_field_with_manual_name(self):
|
||||
class F(FilterSet):
|
||||
book_title = CharFilter(name='title')
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('book_title',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['book_title'].label, "Title")
|
||||
self.assertEqual(f['book_title'].label, "Title")
|
||||
|
||||
def test_form_field_with_manual_name_and_label(self):
|
||||
class F(FilterSet):
|
||||
f1 = CharFilter(name='title', label="Book title")
|
||||
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('f1',)
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['f1'].label, "Book title")
|
||||
self.assertEqual(f['f1'].label, 'Book title')
|
||||
|
||||
def test_filter_with_initial(self):
|
||||
class F(FilterSet):
|
||||
status = ChoiceFilter(choices=STATUS_CHOICES, initial=1)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['status']
|
||||
|
||||
f = F().form
|
||||
self.assertEqual(f.fields['status'].initial, 1)
|
||||
|
||||
def test_form_is_not_bound(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F().form
|
||||
self.assertFalse(f.is_bound)
|
||||
self.assertEqual(f.data, {})
|
||||
|
||||
def test_form_is_bound(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = ('title',)
|
||||
|
||||
f = F({'title': 'Some book'}).form
|
||||
self.assertTrue(f.is_bound)
|
||||
self.assertEqual(f.data, {'title': 'Some book'})
|
||||
|
||||
def test_limit_choices_to(self):
|
||||
User.objects.create(username='inactive', is_active=False, status=REGULAR)
|
||||
User.objects.create(username='active', is_active=True, status=REGULAR)
|
||||
User.objects.create(username='manager', is_active=False, status=MANAGER)
|
||||
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = ManagerGroup
|
||||
fields = ['users', 'manager']
|
||||
f = F().form
|
||||
self.assertEqual(
|
||||
list(f.fields['users'].choices), [(2, 'active')]
|
||||
)
|
||||
self.assertEqual(
|
||||
list(f.fields['manager'].choices), [('', '---------'), (3, 'manager')]
|
||||
)
|
||||
|
||||
def test_disabled_help_text(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Book
|
||||
fields = {
|
||||
# 'in' lookups are CSV-based, which have a `help_text`.
|
||||
'title': ['in']
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
F().form.fields['title__in'].help_text,
|
||||
'Multiple values may be separated by commas.'
|
||||
)
|
||||
|
||||
with override_settings(FILTERS_DISABLE_HELP_TEXT=True):
|
||||
|
||||
self.assertEqual(
|
||||
F().form.fields['title__in'].help_text,
|
||||
''
|
||||
)
|
|
@ -0,0 +1,368 @@
|
|||
import datetime
|
||||
import unittest
|
||||
|
||||
import django
|
||||
from django.db import models
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.timezone import get_default_timezone
|
||||
|
||||
from django_filters import STRICTNESS, FilterSet
|
||||
from django_filters.exceptions import FieldLookupError
|
||||
from django_filters.utils import (
|
||||
get_field_parts,
|
||||
get_model_field,
|
||||
handle_timezone,
|
||||
label_for_filter,
|
||||
raw_validation,
|
||||
resolve_field,
|
||||
verbose_field_name,
|
||||
verbose_lookup_expr
|
||||
)
|
||||
|
||||
from .models import (
|
||||
Account,
|
||||
Article,
|
||||
Book,
|
||||
Business,
|
||||
Company,
|
||||
HiredWorker,
|
||||
NetworkSetting,
|
||||
User
|
||||
)
|
||||
|
||||
|
||||
class GetFieldPartsTests(TestCase):
|
||||
|
||||
def test_field(self):
|
||||
parts = get_field_parts(User, 'username')
|
||||
|
||||
self.assertEqual(len(parts), 1)
|
||||
self.assertIsInstance(parts[0], models.CharField)
|
||||
|
||||
def test_non_existent_field(self):
|
||||
result = get_model_field(User, 'unknown__name')
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_forwards_related_field(self):
|
||||
parts = get_field_parts(User, 'favorite_books__title')
|
||||
|
||||
self.assertEqual(len(parts), 2)
|
||||
self.assertIsInstance(parts[0], models.ManyToManyField)
|
||||
self.assertIsInstance(parts[1], models.CharField)
|
||||
|
||||
def test_reverse_related_field(self):
|
||||
parts = get_field_parts(User, 'manager_of__users__username')
|
||||
|
||||
self.assertEqual(len(parts), 3)
|
||||
self.assertIsInstance(parts[0], ForeignObjectRel)
|
||||
self.assertIsInstance(parts[1], models.ManyToManyField)
|
||||
self.assertIsInstance(parts[2], models.CharField)
|
||||
|
||||
|
||||
class GetModelFieldTests(TestCase):
|
||||
|
||||
def test_non_existent_field(self):
|
||||
result = get_model_field(User, 'unknown__name')
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_related_field(self):
|
||||
result = get_model_field(Business, 'hiredworker__worker')
|
||||
self.assertEqual(result, HiredWorker._meta.get_field('worker'))
|
||||
|
||||
|
||||
class ResolveFieldTests(TestCase):
|
||||
|
||||
def test_resolve_plain_lookups(self):
|
||||
"""
|
||||
Check that the standard query terms can be correctly resolved.
|
||||
eg, an 'EXACT' lookup on a user's username
|
||||
"""
|
||||
model_field = User._meta.get_field('username')
|
||||
lookups = model_field.class_lookups.keys()
|
||||
|
||||
# This is simple - the final ouput of an untransformed field is itself.
|
||||
# The lookups are the default lookups registered to the class.
|
||||
for term in lookups:
|
||||
field, lookup = resolve_field(model_field, term)
|
||||
self.assertIsInstance(field, models.CharField)
|
||||
self.assertEqual(lookup, term)
|
||||
|
||||
def test_resolve_forward_related_lookups(self):
|
||||
"""
|
||||
Check that lookups can be resolved for related fields
|
||||
in the forwards direction.
|
||||
"""
|
||||
lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ]
|
||||
|
||||
# ForeignKey
|
||||
model_field = Article._meta.get_field('author')
|
||||
for term in lookups:
|
||||
field, lookup = resolve_field(model_field, term)
|
||||
self.assertIsInstance(field, models.ForeignKey)
|
||||
self.assertEqual(lookup, term)
|
||||
|
||||
# ManyToManyField
|
||||
model_field = User._meta.get_field('favorite_books')
|
||||
for term in lookups:
|
||||
field, lookup = resolve_field(model_field, term)
|
||||
self.assertIsInstance(field, models.ManyToManyField)
|
||||
self.assertEqual(lookup, term)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), "version does not reverse lookups")
|
||||
def test_resolve_reverse_related_lookups(self):
|
||||
"""
|
||||
Check that lookups can be resolved for related fields
|
||||
in the reverse direction.
|
||||
"""
|
||||
lookups = ['exact', 'gte', 'gt', 'lte', 'lt', 'in', 'isnull', ]
|
||||
|
||||
# ManyToOneRel
|
||||
model_field = User._meta.get_field('article')
|
||||
for term in lookups:
|
||||
field, lookup = resolve_field(model_field, term)
|
||||
self.assertIsInstance(field, models.ManyToOneRel)
|
||||
self.assertEqual(lookup, term)
|
||||
|
||||
# ManyToManyRel
|
||||
model_field = Book._meta.get_field('lovers')
|
||||
for term in lookups:
|
||||
field, lookup = resolve_field(model_field, term)
|
||||
self.assertIsInstance(field, models.ManyToManyRel)
|
||||
self.assertEqual(lookup, term)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||
def test_resolve_transformed_lookups(self):
|
||||
"""
|
||||
Check that chained field transforms are correctly resolved.
|
||||
eg, a 'date__year__gte' lookup on an article's 'published' timestamp.
|
||||
"""
|
||||
# Use a DateTimeField, so we can check multiple transforms.
|
||||
# eg, date__year__gte
|
||||
model_field = Article._meta.get_field('published')
|
||||
|
||||
standard_lookups = [
|
||||
'exact',
|
||||
'iexact',
|
||||
'gte',
|
||||
'gt',
|
||||
'lte',
|
||||
'lt',
|
||||
]
|
||||
|
||||
date_lookups = [
|
||||
'year',
|
||||
'month',
|
||||
'day',
|
||||
'week_day',
|
||||
]
|
||||
|
||||
datetime_lookups = date_lookups + [
|
||||
'hour',
|
||||
'minute',
|
||||
'second',
|
||||
]
|
||||
|
||||
# ex: 'date__gt'
|
||||
for lookup in standard_lookups:
|
||||
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', lookup]))
|
||||
self.assertIsInstance(field, models.DateField)
|
||||
self.assertEqual(resolved_lookup, lookup)
|
||||
|
||||
# ex: 'year__iexact'
|
||||
for part in datetime_lookups:
|
||||
for lookup in standard_lookups:
|
||||
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join([part, lookup]))
|
||||
self.assertIsInstance(field, models.IntegerField)
|
||||
self.assertEqual(resolved_lookup, lookup)
|
||||
|
||||
# ex: 'date__year__lte'
|
||||
for part in date_lookups:
|
||||
for lookup in standard_lookups:
|
||||
field, resolved_lookup = resolve_field(model_field, LOOKUP_SEP.join(['date', part, lookup]))
|
||||
self.assertIsInstance(field, models.IntegerField)
|
||||
self.assertEqual(resolved_lookup, lookup)
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), "version does not support transformed lookup expressions")
|
||||
def test_resolve_implicit_exact_lookup(self):
|
||||
# Use a DateTimeField, so we can check multiple transforms.
|
||||
# eg, date__year__gte
|
||||
model_field = Article._meta.get_field('published')
|
||||
|
||||
field, lookup = resolve_field(model_field, 'date')
|
||||
self.assertIsInstance(field, models.DateField)
|
||||
self.assertEqual(lookup, 'exact')
|
||||
|
||||
field, lookup = resolve_field(model_field, 'date__year')
|
||||
self.assertIsInstance(field, models.IntegerField)
|
||||
self.assertEqual(lookup, 'exact')
|
||||
|
||||
def test_invalid_lookup_expression(self):
|
||||
model_field = Article._meta.get_field('published')
|
||||
|
||||
with self.assertRaises(FieldLookupError) as context:
|
||||
resolve_field(model_field, 'invalid_lookup')
|
||||
|
||||
exc = str(context.exception)
|
||||
self.assertIn(str(model_field), exc)
|
||||
self.assertIn('invalid_lookup', exc)
|
||||
|
||||
def test_invalid_transformed_lookup_expression(self):
|
||||
model_field = Article._meta.get_field('published')
|
||||
|
||||
with self.assertRaises(FieldLookupError) as context:
|
||||
resolve_field(model_field, 'date__invalid_lookup')
|
||||
|
||||
exc = str(context.exception)
|
||||
self.assertIn(str(model_field), exc)
|
||||
self.assertIn('date__invalid_lookup', exc)
|
||||
|
||||
|
||||
class VerboseFieldNameTests(TestCase):
|
||||
|
||||
def test_none(self):
|
||||
verbose_name = verbose_field_name(Article, None)
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
def test_invalid_name(self):
|
||||
verbose_name = verbose_field_name(Article, 'foobar')
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
def test_field(self):
|
||||
verbose_name = verbose_field_name(Article, 'author')
|
||||
self.assertEqual(verbose_name, 'author')
|
||||
|
||||
def test_field_with_verbose_name(self):
|
||||
verbose_name = verbose_field_name(Article, 'name')
|
||||
self.assertEqual(verbose_name, 'title')
|
||||
|
||||
def test_field_all_caps(self):
|
||||
verbose_name = verbose_field_name(NetworkSetting, 'cidr')
|
||||
self.assertEqual(verbose_name, 'CIDR')
|
||||
|
||||
def test_forwards_related_field(self):
|
||||
verbose_name = verbose_field_name(Article, 'author__username')
|
||||
self.assertEqual(verbose_name, 'author username')
|
||||
|
||||
def test_backwards_related_field(self):
|
||||
verbose_name = verbose_field_name(Book, 'lovers__first_name')
|
||||
self.assertEqual(verbose_name, 'lovers first name')
|
||||
|
||||
def test_backwards_related_field_multi_word(self):
|
||||
verbose_name = verbose_field_name(User, 'manager_of')
|
||||
self.assertEqual(verbose_name, 'manager of')
|
||||
|
||||
def test_lazy_text(self):
|
||||
# sanity check
|
||||
field = User._meta.get_field('username')
|
||||
self.assertIsInstance(field.verbose_name, Promise)
|
||||
|
||||
verbose_name = verbose_field_name(User, 'username')
|
||||
self.assertEqual(verbose_name, 'username')
|
||||
|
||||
def test_forwards_fk(self):
|
||||
verbose_name = verbose_field_name(Article, 'author')
|
||||
self.assertEqual(verbose_name, 'author')
|
||||
|
||||
def test_backwards_fk(self):
|
||||
# https://github.com/carltongibson/django-filter/issues/716
|
||||
|
||||
# related_name is set
|
||||
verbose_name = verbose_field_name(Company, 'locations')
|
||||
self.assertEqual(verbose_name, 'locations')
|
||||
|
||||
# related_name not set. Auto-generated relation is `article_set`
|
||||
# _meta.get_field raises FieldDoesNotExist
|
||||
verbose_name = verbose_field_name(User, 'article_set')
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
# WRONG NAME! Returns ManyToOneRel with related_name == None.
|
||||
verbose_name = verbose_field_name(User, 'article')
|
||||
self.assertEqual(verbose_name, '[invalid name]')
|
||||
|
||||
|
||||
class VerboseLookupExprTests(TestCase):
|
||||
|
||||
def test_exact(self):
|
||||
# Exact should default to empty. A verbose expression is unnecessary,
|
||||
# and this behavior works well with list syntax for `Meta.fields`.
|
||||
verbose_lookup = verbose_lookup_expr('exact')
|
||||
self.assertEqual(verbose_lookup, '')
|
||||
|
||||
def test_verbose_expression(self):
|
||||
verbose_lookup = verbose_lookup_expr('date__lt')
|
||||
self.assertEqual(verbose_lookup, 'date is less than')
|
||||
|
||||
def test_missing_keys(self):
|
||||
verbose_lookup = verbose_lookup_expr('foo__bar__lt')
|
||||
self.assertEqual(verbose_lookup, 'foo bar is less than')
|
||||
|
||||
@override_settings(FILTERS_VERBOSE_LOOKUPS={'exact': 'is equal to'})
|
||||
def test_overridden_settings(self):
|
||||
verbose_lookup = verbose_lookup_expr('exact')
|
||||
self.assertEqual(verbose_lookup, 'is equal to')
|
||||
|
||||
|
||||
class LabelForFilterTests(TestCase):
|
||||
|
||||
def test_standard_label(self):
|
||||
label = label_for_filter(Article, 'name', 'in')
|
||||
self.assertEqual(label, 'Title is in')
|
||||
|
||||
def test_related_model(self):
|
||||
label = label_for_filter(Article, 'author__first_name', 'in')
|
||||
self.assertEqual(label, 'Author first name is in')
|
||||
|
||||
def test_exclusion_label(self):
|
||||
label = label_for_filter(Article, 'name', 'in', exclude=True)
|
||||
self.assertEqual(label, 'Exclude title is in')
|
||||
|
||||
def test_related_model_exclusion(self):
|
||||
label = label_for_filter(Article, 'author__first_name', 'in', exclude=True)
|
||||
self.assertEqual(label, 'Exclude author first name is in')
|
||||
|
||||
def test_exact_lookup(self):
|
||||
label = label_for_filter(Article, 'name', 'exact')
|
||||
self.assertEqual(label, 'Title')
|
||||
|
||||
def test_field_all_caps(self):
|
||||
label = label_for_filter(NetworkSetting, 'cidr', 'contains', exclude=True)
|
||||
self.assertEqual(label, 'Exclude CIDR contains')
|
||||
|
||||
|
||||
class HandleTimezone(TestCase):
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='America/Sao_Paulo')
|
||||
def test_handle_dst_ending(self):
|
||||
dst_ending_date = datetime.datetime(2017, 2, 18, 23, 59, 59, 999999)
|
||||
handled = handle_timezone(dst_ending_date, False)
|
||||
self.assertEqual(handled, get_default_timezone().localize(dst_ending_date, False))
|
||||
|
||||
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
|
||||
@override_settings(TIME_ZONE='America/Sao_Paulo')
|
||||
def test_handle_dst_starting(self):
|
||||
dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0)
|
||||
handled = handle_timezone(dst_starting_date, True)
|
||||
self.assertEqual(handled, get_default_timezone().localize(dst_starting_date, True))
|
||||
|
||||
|
||||
class RawValidationDataTests(TestCase):
|
||||
def test_simple(self):
|
||||
class F(FilterSet):
|
||||
class Meta:
|
||||
model = Article
|
||||
fields = ['id', 'author', 'name']
|
||||
strict = STRICTNESS.RAISE_VALIDATION_ERROR
|
||||
|
||||
f = F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'})
|
||||
with self.assertRaises(ValidationError) as exc:
|
||||
f.qs
|
||||
|
||||
self.assertDictEqual(raw_validation(exc.exception), {
|
||||
'id': ['Enter a number.'],
|
||||
'author': ['Select a valid choice. That choice is not one of the available choices.'],
|
||||
})
|
|
@ -0,0 +1,109 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase, override_settings
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
from django_filters.filterset import FilterSet, filterset_factory
|
||||
from django_filters.views import FilterView
|
||||
|
||||
from .models import Book
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='tests.urls')
|
||||
class GenericViewTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
Book.objects.create(
|
||||
title="Ender's Game", price='1.00', average_rating=3.0)
|
||||
Book.objects.create(
|
||||
title="Rainbow Six", price='1.00', average_rating=3.0)
|
||||
Book.objects.create(
|
||||
title="Snowcrash", price='1.00', average_rating=3.0)
|
||||
|
||||
|
||||
class GenericClassBasedViewTests(GenericViewTestCase):
|
||||
base_url = '/books/'
|
||||
|
||||
def test_view(self):
|
||||
response = self.client.get(self.base_url)
|
||||
for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']:
|
||||
self.assertContains(response, b)
|
||||
|
||||
def test_view_filtering_on_title(self):
|
||||
response = self.client.get(self.base_url + '?title=Snowcrash')
|
||||
for b in ['Ender's Game', 'Rainbow Six']:
|
||||
self.assertNotContains(response, b)
|
||||
self.assertContains(response, 'Snowcrash')
|
||||
|
||||
def test_view_with_filterset_not_model(self):
|
||||
factory = RequestFactory()
|
||||
request = factory.get(self.base_url)
|
||||
filterset = filterset_factory(Book)
|
||||
view = FilterView.as_view(filterset_class=filterset)
|
||||
response = view(request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
for b in ['Ender'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)
|
||||
view = FilterView.as_view()
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
view(request)
|
||||
|
||||
def test_view_with_bad_filterset(self):
|
||||
class MyFilterSet(FilterSet):
|
||||
pass
|
||||
|
||||
factory = RequestFactory()
|
||||
request = factory.get(self.base_url)
|
||||
view = FilterView.as_view(filterset_class=MyFilterSet)
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
view(request)
|
||||
|
||||
|
||||
class GenericFunctionalViewTests(GenericViewTestCase):
|
||||
base_url = '/books-legacy/'
|
||||
|
||||
def test_view(self):
|
||||
response = self.client.get(self.base_url)
|
||||
for b in ['Ender's Game', 'Rainbow Six', 'Snowcrash']:
|
||||
self.assertContains(response, b)
|
||||
# extra context
|
||||
self.assertEqual(response.context_data['foo'], 'bar')
|
||||
self.assertEqual(response.context_data['bar'], 'foo')
|
||||
|
||||
def test_view_filtering_on_price(self):
|
||||
response = self.client.get(self.base_url + '?title=Snowcrash')
|
||||
for b in ['Ender's Game', 'Rainbow Six']:
|
||||
self.assertNotContains(response, b)
|
||||
self.assertContains(response, 'Snowcrash')
|
|
@ -0,0 +1,422 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.forms import Select, TextInput
|
||||
from django.test import TestCase
|
||||
|
||||
from django_filters.widgets import (
|
||||
BaseCSVWidget,
|
||||
BooleanWidget,
|
||||
CSVWidget,
|
||||
LinkWidget,
|
||||
LookupTypeWidget,
|
||||
QueryArrayWidget,
|
||||
RangeWidget,
|
||||
SuffixedMultiWidget
|
||||
)
|
||||
|
||||
|
||||
class LookupTypeWidgetTests(TestCase):
|
||||
|
||||
def test_widget_requires_field(self):
|
||||
with self.assertRaises(TypeError):
|
||||
LookupTypeWidget()
|
||||
|
||||
def test_widget_render(self):
|
||||
widgets = [TextInput(), Select(choices=(('a', 'a'), ('b', 'b')))]
|
||||
w = LookupTypeWidget(widgets)
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input name="price_0" type="text" />
|
||||
<select name="price_1">
|
||||
<option value="a">a</option>
|
||||
<option value="b">b</option>
|
||||
</select>""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', None), """
|
||||
<input name="price_0" type="text" />
|
||||
<select name="price_1">
|
||||
<option value="a">a</option>
|
||||
<option value="b">b</option>
|
||||
</select>""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', ['2', 'a']), """
|
||||
<input name="price_0" type="text" value="2" />
|
||||
<select name="price_1">
|
||||
<option selected="selected" value="a">a</option>
|
||||
<option value="b">b</option>
|
||||
</select>""")
|
||||
|
||||
|
||||
class LinkWidgetTests(TestCase):
|
||||
|
||||
def test_widget_without_choices(self):
|
||||
w = LinkWidget()
|
||||
self.assertEqual(len(w.choices), 0)
|
||||
self.assertHTMLEqual(w.render('price', ''), """<ul />""")
|
||||
|
||||
def test_widget(self):
|
||||
choices = (
|
||||
('test-val1', 'test-label1'),
|
||||
('test-val2', 'test-label2'),
|
||||
)
|
||||
w = LinkWidget(choices=choices)
|
||||
self.assertEqual(len(w.choices), 2)
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<ul>
|
||||
<li><a href="?price=test-val1">test-label1</a></li>
|
||||
<li><a href="?price=test-val2">test-label2</a></li>
|
||||
</ul>""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', None), """
|
||||
<ul>
|
||||
<li><a href="?price=test-val1">test-label1</a></li>
|
||||
<li><a href="?price=test-val2">test-label2</a></li>
|
||||
</ul>""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', 'test-val1'), """
|
||||
<ul>
|
||||
<li><a class="selected"
|
||||
href="?price=test-val1">test-label1</a></li>
|
||||
<li><a href="?price=test-val2">test-label2</a></li>
|
||||
</ul>""")
|
||||
|
||||
def test_widget_with_option_groups(self):
|
||||
choices = (
|
||||
('Audio', (
|
||||
('vinyl', 'Vinyl'),
|
||||
('cd', 'CD'),
|
||||
)),
|
||||
('Video', (
|
||||
('vhs', 'VHS Tape'),
|
||||
('dvd', 'DVD'),
|
||||
)),
|
||||
('unknown', 'Unknown'),
|
||||
)
|
||||
|
||||
w = LinkWidget(choices=choices)
|
||||
self.assertHTMLEqual(w.render('media', ''), """
|
||||
<ul>
|
||||
<li><a href="?media=vinyl">Vinyl</a></li>
|
||||
<li><a href="?media=cd">CD</a></li>
|
||||
<li><a href="?media=vhs">VHS Tape</a></li>
|
||||
<li><a href="?media=dvd">DVD</a></li>
|
||||
<li><a href="?media=unknown">Unknown</a></li>
|
||||
</ul>""")
|
||||
|
||||
def test_widget_with_blank_choice(self):
|
||||
choices = (
|
||||
('', '---------'),
|
||||
('test-val1', 'test-label1'),
|
||||
('test-val2', 'test-label2'),
|
||||
)
|
||||
|
||||
w = LinkWidget(choices=choices)
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<ul>
|
||||
<li><a class="selected" href="?price=">All</a></li>
|
||||
<li><a href="?price=test-val1">test-label1</a></li>
|
||||
<li><a href="?price=test-val2">test-label2</a></li>
|
||||
</ul>""")
|
||||
|
||||
def test_widget_value_from_datadict(self):
|
||||
w = LinkWidget()
|
||||
data = {'price': 'test-val1'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, 'test-val1')
|
||||
|
||||
|
||||
class SuffixedMultiWidgetTests(TestCase):
|
||||
def test_assertions(self):
|
||||
# number of widgets must match suffixes
|
||||
with self.assertRaises(AssertionError):
|
||||
SuffixedMultiWidget(widgets=[BooleanWidget])
|
||||
|
||||
# suffixes must be unique
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = ['a', 'a']
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
W(widgets=[BooleanWidget, BooleanWidget])
|
||||
|
||||
# should succeed
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = ['a', 'b']
|
||||
W(widgets=[BooleanWidget, BooleanWidget])
|
||||
|
||||
def test_render(self):
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = ['min', 'max']
|
||||
|
||||
w = W(widgets=[TextInput, TextInput])
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input name="price_min" type="text" />
|
||||
<input name="price_max" type="text" />
|
||||
""")
|
||||
|
||||
# blank suffix
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = [None, 'lookup']
|
||||
|
||||
w = W(widgets=[TextInput, TextInput])
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input name="price" type="text" />
|
||||
<input name="price_lookup" type="text" />
|
||||
""")
|
||||
|
||||
def test_value_from_datadict(self):
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = ['min', 'max']
|
||||
|
||||
w = W(widgets=[TextInput, TextInput])
|
||||
result = w.value_from_datadict({
|
||||
'price_min': '1',
|
||||
'price_max': '2',
|
||||
}, {}, 'price')
|
||||
self.assertEqual(result, ['1', '2'])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, [None, None])
|
||||
|
||||
# blank suffix
|
||||
class W(SuffixedMultiWidget):
|
||||
suffixes = ['', 'lookup']
|
||||
|
||||
w = W(widgets=[TextInput, TextInput])
|
||||
result = w.value_from_datadict({
|
||||
'price': '1',
|
||||
'price_lookup': 'lt',
|
||||
}, {}, 'price')
|
||||
self.assertEqual(result, ['1', 'lt'])
|
||||
|
||||
|
||||
class RangeWidgetTests(TestCase):
|
||||
|
||||
def test_widget(self):
|
||||
w = RangeWidget()
|
||||
self.assertEqual(len(w.widgets), 2)
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input type="text" name="price_0" />
|
||||
-
|
||||
<input type="text" name="price_1" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', slice(5.99, 9.99)), """
|
||||
<input type="text" name="price_0" value="5.99" />
|
||||
-
|
||||
<input type="text" name="price_1" value="9.99" />""")
|
||||
|
||||
def test_widget_attributes(self):
|
||||
w = RangeWidget(attrs={'type': 'date'})
|
||||
self.assertEqual(len(w.widgets), 2)
|
||||
self.assertHTMLEqual(w.render('date', ''), """
|
||||
<input type="date" name="date_0" />
|
||||
-
|
||||
<input type="date" name="date_1" />""")
|
||||
|
||||
|
||||
class BooleanWidgetTests(TestCase):
|
||||
"""
|
||||
"""
|
||||
def test_widget_render(self):
|
||||
w = BooleanWidget()
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<select name="price">
|
||||
<option selected="selected" value="">Unknown</option>
|
||||
<option value="true">Yes</option>
|
||||
<option value="false">No</option>
|
||||
</select>""")
|
||||
|
||||
def test_widget_value_from_datadict(self):
|
||||
"""
|
||||
"""
|
||||
w = BooleanWidget()
|
||||
|
||||
trueActive = {'active': 'true'}
|
||||
result = w.value_from_datadict(trueActive, {}, 'active')
|
||||
self.assertEqual(result, True)
|
||||
|
||||
falseActive = {'active': 'false'}
|
||||
result = w.value_from_datadict(falseActive, {}, 'active')
|
||||
self.assertEqual(result, False)
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'active')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class CSVWidgetTests(TestCase):
|
||||
def test_widget(self):
|
||||
w = CSVWidget()
|
||||
self.assertHTMLEqual(w.render('price', None), """
|
||||
<input type="text" name="price" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', ''), """
|
||||
<input type="text" name="price" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', []), """
|
||||
<input type="text" name="price" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', '1'), """
|
||||
<input type="text" name="price" value="1" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', '1,2'), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', ['1', '2']), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', [1, 2]), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
def test_widget_value_from_datadict(self):
|
||||
w = CSVWidget()
|
||||
|
||||
data = {'price': None}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
data = {'price': '1'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price': '1,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1', '2'])
|
||||
|
||||
data = {'price': '1,,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1', '', '2'])
|
||||
|
||||
data = {'price': '1,'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1', ''])
|
||||
|
||||
data = {'price': ','}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['', ''])
|
||||
|
||||
data = {'price': ''}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, None)
|
||||
|
||||
|
||||
class CSVSelectTests(TestCase):
|
||||
class CSVSelect(BaseCSVWidget, Select):
|
||||
pass
|
||||
|
||||
def test_widget(self):
|
||||
w = self.CSVSelect(choices=((1, 'a'), (2, 'b')))
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', None),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', ''),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
""")
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', '1'),
|
||||
"""
|
||||
<select name="price">
|
||||
<option selected="selected" value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
""")
|
||||
|
||||
self.assertHTMLEqual(
|
||||
w.render('price', '1,2'),
|
||||
"""
|
||||
<select name="price">
|
||||
<option value="1">a</option>
|
||||
<option value="2">b</option>
|
||||
</select>
|
||||
"""
|
||||
)
|
||||
|
||||
self.assertHTMLEqual(w.render('price', ['1', '2']), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
self.assertHTMLEqual(w.render('price', [1, 2]), """
|
||||
<input type="text" name="price" value="1,2" />""")
|
||||
|
||||
|
||||
class QueryArrayWidgetTests(TestCase):
|
||||
|
||||
def test_widget_value_from_datadict(self):
|
||||
w = QueryArrayWidget()
|
||||
|
||||
# Values can be provided as csv string: ?foo=bar,baz
|
||||
data = {'price': None}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price': '1'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price': '1,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price': '1,,2'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price': '1,'}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price': ','}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price': ''}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
# Values can be provided as query array: ?foo[]=bar&foo[]=baz
|
||||
|
||||
data = {'price[]': None}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price[]': ['1']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price[]': ['1', '2']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price[]': ['1', '', '2']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(sorted(result), ['1', '2'])
|
||||
|
||||
data = {'price[]': ['1', '']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, ['1'])
|
||||
|
||||
data = {'price[]': ['', '']}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
data = {'price[]': []}
|
||||
result = w.value_from_datadict(data, {}, 'price')
|
||||
self.assertEqual(result, [])
|
||||
|
||||
result = w.value_from_datadict({}, {}, 'price')
|
||||
self.assertEqual(result, [])
|
|
@ -0,0 +1,16 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from django_filters.views import FilterView, object_filter
|
||||
|
||||
from .models import Book
|
||||
|
||||
|
||||
def _foo():
|
||||
return 'bar'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^books-legacy/$', object_filter, {'model': Book, 'extra_context': {'foo': _foo, 'bar': 'foo'}}),
|
||||
url(r'^books/$', FilterView.as_view(model=Book)),
|
||||
]
|
Loading…
Reference in New Issue