Compare commits

...

No commits in common. "master" and "sid" have entirely different histories.
master ... sid

83 changed files with 15065 additions and 982 deletions

View File

@ -1,21 +0,0 @@
[bumpversion]
current_version = 1.0.1
commit = False
tag = False
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?
serialize =
{major}.{minor}.{patch}-{release}
{major}.{minor}.{patch}
[bumpversion:file:django_filters/__init__.py]
[bumpversion:file:setup.py]
[bumpversion:file:docs/conf.py]
[bumpversion:part:release]
optional_value = gamma
values =
dev
gamma

7
.gitignore vendored
View File

@ -1,7 +0,0 @@
*.pyc
*.egg-info
build/
dist/
docs/_build
.python-version
.tox

View File

@ -1,16 +0,0 @@
sudo: false
language: python
python: '3.5'
cache: pip
install:
- pip install coverage tox
script:
- coverage erase
- tox
after_success:
- coverage combine
- coverage report

View File

@ -1,3 +1,55 @@
Version 1.1 (2017-10-19)
------------------------
* Add Deprecations for 2.0 (#792)
* Improve IsoDateTimeField test clarity (#790)
* Fix form attr references in tests (#789)
* Simplify tox config, drop python 3.3 & django 1.8 (#787)
* Make get_filter_name a classmethod, allowing it to be overriden for each FilterClass (#775)
* Support active timezone (#750)
* Docs Typo: django_filters -> filters in docs (#773)
* Add Polish translations for some messages (#771)
* Remove support for Django 1.9 (EOL) (#752)
* Use required attribute from field when getting schema fields (#766)
* Prevent circular ImportError hiding for rest_framework sub-package (#741)
* Deprecate 'extra' field attrs on Filter (#734)
* Add SuffixedMultiWidget (#681)
* Fix null filtering for *Choice filters (#680)
* Use isort on imports (#761)
* Use urlencode from django.utils.http (#760)
* Remove OrderingFilter.help_text (#757)
* Update DRF test dependency to 3.6 (#747)
Version 1.0.4 (2017-05-19)
--------------------------
Quick fix for verbose_field_name issue from 1.0.3 (#722)
Version 1.0.3 (2017-05-16)
--------------------------
Improves compatibility with Django REST Framework schema generation.
See the `1.0.3 Milestone`__ for full details.
__ https://github.com/carltongibson/django-filter/milestone/13?closed=1
Version 1.0.2 (2017-03-20)
--------------------------
Updates for compatibility with Django 1.11 and Django REST Framework 3.6.
Adds CI testing against Python 3.6
See the `1.0.2 Milestone`__ for full details.
__ https://github.com/carltongibson/django-filter/milestone/12?closed=1
Version 1.0.1 (2016-11-28)
--------------------------

View File

@ -8,4 +8,7 @@ recursive-include docs *
recursive-include requirements *
recursive-include tests *
recursive-include django_filters/locale *
prune docs/_build
recursive-include django_filters/templates *.html
prune docs/_build
global-exclude __pycache__
global-exclude *.py[co]

View File

@ -1,11 +0,0 @@
.PHONY: deps, test, clean
deps:
pip install -r ./requirements/test.txt
test:
./runtests.py
clean:
rm -r build dist django_filter.egg-info

130
PKG-INFO Normal file
View File

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

View File

@ -1,20 +1,27 @@
Django Filter
=============
Django-filter is a reusable Django application for allowing users to filter
querysets dynamically.
Django-filter is a reusable Django application allowing users to declaratively
add dynamic ``QuerySet`` filtering from URL parameters.
Full documentation on `read the docs`_.
.. image:: https://travis-ci.org/carltongibson/django-filter.svg?branch=master
:target: https://travis-ci.org/carltongibson/django-filter
.. image:: https://codecov.io/gh/carltongibson/django-filter/branch/develop/graph/badge.svg
:target: https://codecov.io/gh/carltongibson/django-filter
.. image:: https://badge.fury.io/py/django-filter.svg
:target: http://badge.fury.io/py/django-filter
Requirements
------------
* **Python**: 2.7, 3.3, 3.4, 3.5
* **Django**: 1.8, 1.9, 1.10
* **DRF**: 3.4, 3.5
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.8, 1.10, 1.11
* **DRF**: 3.7
Installation
------------
@ -25,11 +32,15 @@ Install using pip:
pip install django-filter
Or clone the repo and add to your ``PYTHONPATH``:
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. code-block:: sh
.. code-block:: python
INSTALLED_APPS = [
...
'django_filters',
]
git clone git@github.com:carltongibson/django-filter.git
Usage
-----
@ -57,22 +68,28 @@ And then in your view you could do:
filter = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': filter})
Django-filters additionally supports specifying ``FilterSet`` fields using
a dictionary to specify filters with lookup types:
Usage with Django REST Framework
--------------------------------
Django-filter provides a custom ``FilterSet`` and filter backend for use with
Django REST Framework.
To use this adjust your import to use
``django_filters.rest_framework.FilterSet``.
.. code-block:: python
import django_filters
from django_filters import rest_framework as filters
class ProductFilter(django_filters.FilterSet):
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = {'name': ['exact', 'icontains'],
'price': ['exact', 'gte', 'lte'],
}
fields = ('category', 'in_stock')
For more details see the `DRF integration docs`_.
The filters will be available as ``'name'``, ``'name__icontains'``,
``'price'``, ``'price__gte'``, and ``'price__lte'`` in the above example.
Support
-------
@ -82,3 +99,4 @@ If you have questions about usage or development you can join the
.. _`read the docs`: https://django-filter.readthedocs.io/en/develop/
.. _`mailing list`: http://groups.google.com/group/django-filter
.. _`DRF integration docs`: https://django-filter.readthedocs.io/en/develop/guide/rest_framework.html

147
debian/changelog vendored
View File

@ -1,5 +1,146 @@
django-filter (0.11.0-0) wheezy-eobuilder; urgency=low
django-filter (1.1.0-1) unstable; urgency=medium
* Initial package
* New upstream version 1.1.0
* Refresh patches
-- Benjamin Dauvergne <bdauvergne@entrouvert.com> Thu, 12 Oct 2015 21:00:32 +0200
-- Antonio Terceiro <terceiro@debian.org> Wed, 10 Jan 2018 10:50:46 -0200
django-filter (1.0.4-2) unstable; urgency=medium
* Drop transitional packages django-filter and python-django-filter.
Closes: #878389.
-- Brian May <bam@debian.org> Tue, 31 Oct 2017 07:53:45 +1100
django-filter (1.0.4-1) unstable; urgency=medium
[ Brian May ]
* Remove broken symlink. Closes: #857825.
* Fix minor lintian warnings.
* New upstream version. Closes: #865802, closes: #863024.
[ Michael Fladischer ]
* Add python(3)-djangorestframework and python(3)-django-crispy-forms
to Build-Depends so tests can run successfully.
* Clean up modified files in docs/.build and django_filter.egg-info to
allow two builds in a row.
* Use https:// for copyright-format 1.0 URL.
* Reformat packaging files with cme for better readability and remove
unnecessary versioned dependencies.
* Add unique parts to short and long descriptions.
* Switch to python3-sphinx.
* Rebuild all django.mo files from source using python3-babel.
* Add patch to fix value of Language fields for django.po files.
-- Brian May <bam@debian.org> Sat, 29 Jul 2017 17:09:04 +1000
django-filter (0.13.0-1) unstable; urgency=medium
[ Ondřej Nový ]
* Fixed homepage (https)
[ Brian May ]
* New upstream version.
-- Brian May <bam@debian.org> Wed, 06 Apr 2016 12:39:17 +1000
django-filter (0.11.0-2) unstable; urgency=medium
* Run tests for all Python 3.*
-- Brian May <bam@debian.org> Mon, 26 Oct 2015 13:00:58 +1100
django-filter (0.11.0-1) unstable; urgency=medium
* New upstream version.
* Add fix_tests.patch, applied upstream see
https://github.com/alex/django-filter/pull/287
Closes: #796754: FTBFS: test_filtering_uses_distinct raises AttributeError
-- Brian May <bam@debian.org> Mon, 24 Aug 2015 11:52:41 +1000
django-filter (0.9.2-1) unstable; urgency=medium
* New upstream version.
* Source package comes with prebuilt docs which include jquery.js and
underscore.js, so provide sources to keep Lintian happy. Note none of these
files are not used for the built Debian package.
-- Brian May <bam@debian.org> Fri, 10 Apr 2015 10:38:43 +1000
django-filter (0.7-4) unstable; urgency=low
* Add patch to fix tests in Django 1.7. Closes: #755635.
-- Brian May <bam@debian.org> Mon, 11 Aug 2014 10:05:52 +1000
django-filter (0.7-3) unstable; urgency=low
* Python3 package.
-- Brian May <bam@debian.org> Thu, 03 Jul 2014 13:55:03 +1000
django-filter (0.7-1) unstable; urgency=low
* New upstream version.
-- Brian May <bam@debian.org> Fri, 04 Apr 2014 09:40:31 +1100
django-filter (0.6-3) unstable; urgency=low
* python-django-filters-doc now has have Breaks+Replaces:
python-django-filter-doc (<< 0.6-2). Closes: #720046.
-- Brian May <bam@debian.org> Fri, 02 Aug 2013 15:47:04 +1000
django-filter (0.6-2) unstable; urgency=low
* Rename python-django-filter to python-django-filters to make
it compliant with Debian Python policy.
* Provide transitional dummy package.
-- Brian May <bam@debian.org> Fri, 02 Aug 2013 11:42:17 +1000
django-filter (0.6-1) unstable; urgency=low
[ Michael Fladischer ]
* New upstream release.
* Set DPMT as Uploaders.
* Switch to dh_python2.
* Bump Standards version to 3.9.4.
* Bump debhelper Build-Depends to >= 8.1.0~.
* Rename to python-django-filter, provide transitional dummy package
* Build documentation with sphinx and move it to separate package
python-django-filter-doc.
* Drop patches.
* Change short and long description.
* Make dependencies on python-django versioned.
* Drop debian/pycompat.
* Add X-Python-Version.
* Use DEP5 format for d/copyright.
django-filter.
* Add d/watch file.
* Move Homepage field to source section.
-- Brian May <bam@debian.org> Mon, 17 Jun 2013 13:53:11 +1000
django-filter (0.5.3-3) unstable; urgency=low
* Clean out debian/patches/debian-changes-0.5.3-2 and fix FTBS.
Closes: #643092.
* Increase standards version to 3.9.2 from 3.9.1
* Add patch from upstream to make XMLField optional.
-- Brian May <bam@debian.org> Wed, 12 Oct 2011 09:50:46 +1100
django-filter (0.5.3-2) unstable; urgency=low
* Remove redundant line from debian/rules.
-- Brian May <bam@debian.org> Fri, 08 Apr 2011 10:41:00 +1000
django-filter (0.5.3-1) unstable; urgency=low
* Initial release. Closes: #619195.
-- Brian May <bam@debian.org> Mon, 28 Mar 2011 13:41:07 +1100

2
debian/clean vendored Normal file
View File

@ -0,0 +1,2 @@
django_filter.egg-info/SOURCES.txt
django_filters/locale/*/LC_MESSAGES/django.mo

2
debian/compat vendored
View File

@ -1 +1 @@
7
10

73
debian/control vendored
View File

@ -1,12 +1,73 @@
Source: django-filter
Maintainer: Entr'ouvert <info@entrouvert.com>
Maintainer: Brian May <bam@debian.org>
Uploaders: Debian Python Modules Team <python-modules-team@lists.alioth.debian.org>
Section: python
Priority: optional
Build-Depends: python-setuptools (>= 0.6b3), python3-setuptools, python-all (>= 2.6.6-3), python3-all, debhelper (>= 7)
Standards-Version: 3.9.1
X-Python-Version: >= 2.7
Build-Depends: debhelper (>=10),
dh-python,
python-all,
python-django,
python-django-crispy-forms,
python-djangorestframework,
python-mock,
python-setuptools,
python3-all,
python3-django,
python3-django-crispy-forms,
python3-djangorestframework,
python3-mock,
python3-setuptools,
python3-sphinx-rtd-theme
Build-Depends-Indep: libjs-jquery,
python3-babel,
python3-sphinx
Standards-Version: 4.0.0
Vcs-Browser: https://anonscm.debian.org/cgit/python-modules/packages/django-filter.git
Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/django-filter.git
Homepage: https://github.com/alex/django-filter
X-Python-Version: >= 2.6
Package: python-django-filters
Architecture: all
Depends: ${misc:Depends}, ${python:Depends}, python-django (>= 1.4.5)
Description: Django-filter is a reusable Django application for allowing users to filter querysets dynamically.
Depends: python-django,
${misc:Depends},
${python:Depends}
Suggests: python-django-filters-doc
Breaks: django-filter (<< 0.6),
python-django-filter (<< 0.6-2)
Replaces: django-filter (<< 0.6),
python-django-filter (<< 0.6-2)
Description: filter Django QuerySets based on user selections (Python2 version)
Django-filter is a generic, reusable application to alleviate some of the more
mundane bits of view code. Specifically allowing the users to filter down a
queryset based on a models fields and displaying the form to let them do this.
.
This package contains the Python 2 version of the library.
Package: python3-django-filters
Architecture: all
Depends: python3-django,
${misc:Depends},
${python3:Depends}
Suggests: python-django-filters-doc
Description: filter Django QuerySets based on user selections (Python3 version)
Django-filter is a generic, reusable application to alleviate some of the more
mundane bits of view code. Specifically allowing the users to filter down a
queryset based on a models fields and displaying the form to let them do this.
.
This package contains the Python 3 version of the library.
Package: python-django-filters-doc
Architecture: all
Section: doc
Depends: ${misc:Depends},
${sphinxdoc:Depends}
Breaks: python-django-filter-doc (<< 0.6-2)
Replaces: python-django-filter-doc (<< 0.6-2)
Description: filter Django QuerySets based on user selections (Documentation)
Django-filter is a generic, reusable application to alleviate some of the more
mundane bits of view code. Specifically allowing the users to filter down a
queryset based on a models fields and displaying the form to let them do this.
.
This package contains the documentation.

36
debian/copyright vendored Normal file
View File

@ -0,0 +1,36 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: django-filter
Upstream-Contact: Alex Gaynor <alex.gaynor@gmail.com>
Source: https://pypi.python.org/pypi/django-filter
Files: *
Copyright: 2009, Alex Gaynor <alex.gaynor@gmail.com>
License: BSD-django-filter
Files: debian/*
Copyright: 2011, Brian May <bam@debian.org>
2013, Fladischer Michael <FladischerMichael@fladi.at>
License: BSD-django-filter
License: BSD-django-filter
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of its contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
debian/gbp.conf vendored Normal file
View File

@ -0,0 +1,2 @@
[DEFAULT]
debian-branch=debian/master

9472
debian/missing-sources/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

999
debian/missing-sources/underscore.js vendored Normal file
View File

@ -0,0 +1,999 @@
// Underscore.js 1.3.1
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
// For all details and documentation:
// http://documentcloud.github.com/underscore
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Establish the object that gets returned to break out of a loop iteration.
var breaker = {};
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var slice = ArrayProto.slice,
unshift = ArrayProto.unshift,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var
nativeForEach = ArrayProto.forEach,
nativeMap = ArrayProto.map,
nativeReduce = ArrayProto.reduce,
nativeReduceRight = ArrayProto.reduceRight,
nativeFilter = ArrayProto.filter,
nativeEvery = ArrayProto.every,
nativeSome = ArrayProto.some,
nativeIndexOf = ArrayProto.indexOf,
nativeLastIndexOf = ArrayProto.lastIndexOf,
nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeBind = FuncProto.bind;
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) { return new wrapper(obj); };
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for the old `require()` API. If we're in
// the browser, add `_` as a global object via a string identifier,
// for Closure Compiler "advanced" mode.
if (typeof exports !== 'undefined') {
if (typeof module !== 'undefined' && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root['_'] = _;
}
// Current version.
_.VERSION = '1.3.1';
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles objects with the built-in `forEach`, arrays, and raw objects.
// Delegates to **ECMAScript 5**'s native `forEach` if available.
var each = _.each = _.forEach = function(obj, iterator, context) {
if (obj == null) return;
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
// Return the results of applying the iterator to each element.
// Delegates to **ECMAScript 5**'s native `map` if available.
_.map = _.collect = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
if (obj.length === +obj.length) results.length = obj.length;
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
}
each(obj, function(value, index, list) {
if (!initial) {
memo = value;
initial = true;
} else {
memo = iterator.call(context, memo, value, index, list);
}
});
if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
var reversed = _.toArray(obj).reverse();
if (context && !initial) iterator = _.bind(iterator, context);
return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, iterator, context) {
var result;
any(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) {
result = value;
return true;
}
});
return result;
};
// Return all the elements that pass a truth test.
// Delegates to **ECMAScript 5**'s native `filter` if available.
// Aliased as `select`.
_.filter = _.select = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (!iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
// Determine whether all of the elements match a truth test.
// Delegates to **ECMAScript 5**'s native `every` if available.
// Aliased as `all`.
_.every = _.all = function(obj, iterator, context) {
var result = true;
if (obj == null) return result;
if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
each(obj, function(value, index, list) {
if (!(result = result && iterator.call(context, value, index, list))) return breaker;
});
return result;
};
// Determine if at least one element in the object matches a truth test.
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
// Determine if a given value is included in the array or object using `===`.
// Aliased as `contains`.
_.include = _.contains = function(obj, target) {
var found = false;
if (obj == null) return found;
if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
found = any(obj, function(value) {
return value === target;
});
return found;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
});
};
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, function(value){ return value[key]; });
};
// Return the maximum element or (element-based computation).
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return -Infinity;
var result = {computed : -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iterator, context) {
if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
if (!iterator && _.isEmpty(obj)) return Infinity;
var result = {computed : Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed < result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
// Shuffle an array.
_.shuffle = function(obj) {
var shuffled = [], rand;
each(obj, function(value, index, list) {
if (index == 0) {
shuffled[0] = value;
} else {
rand = Math.floor(Math.random() * (index + 1));
shuffled[index] = shuffled[rand];
shuffled[rand] = value;
}
});
return shuffled;
};
// Sort the object's values by a criterion produced by an iterator.
_.sortBy = function(obj, iterator, context) {
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
criteria : iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria, b = right.criteria;
return a < b ? -1 : a > b ? 1 : 0;
}), 'value');
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, val) {
var result = {};
var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };
each(obj, function(value, index) {
var key = iterator(value, index);
(result[key] || (result[key] = [])).push(value);
});
return result;
};
// Use a comparator function to figure out at what index an object should
// be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iterator) {
iterator || (iterator = _.identity);
var low = 0, high = array.length;
while (low < high) {
var mid = (low + high) >> 1;
iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
}
return low;
};
// Safely convert anything iterable into a real, live array.
_.toArray = function(iterable) {
if (!iterable) return [];
if (iterable.toArray) return iterable.toArray();
if (_.isArray(iterable)) return slice.call(iterable);
if (_.isArguments(iterable)) return slice.call(iterable);
return _.values(iterable);
};
// Return the number of elements in an object.
_.size = function(obj) {
return _.toArray(obj).length;
};
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head`. The **guard** check allows it to work
// with `_.map`.
_.first = _.head = function(array, n, guard) {
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
};
// Returns everything but the last entry of the array. Especcialy useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N. The **guard** check allows it to work with
// `_.map`.
_.initial = function(array, n, guard) {
return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
return array[array.length - 1];
}
};
// Returns everything but the first entry of the array. Aliased as `tail`.
// Especially useful on the arguments object. Passing an **index** will return
// the rest of the values in the array from that index onward. The **guard**
// check allows it to work with `_.map`.
_.rest = _.tail = function(array, index, guard) {
return slice.call(array, (index == null) || guard ? 1 : index);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, function(value){ return !!value; });
};
// Return a completely flattened version of an array.
_.flatten = function(array, shallow) {
return _.reduce(array, function(memo, value) {
if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));
memo[memo.length] = value;
return memo;
}, []);
};
// Return a version of the array that does not contain the specified value(s).
_.without = function(array) {
return _.difference(array, slice.call(arguments, 1));
};
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iterator) {
var initial = iterator ? _.map(array, iterator) : array;
var result = [];
_.reduce(initial, function(memo, el, i) {
if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {
memo[memo.length] = el;
result[result.length] = array[i];
}
return memo;
}, []);
return result;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(_.flatten(arguments, true));
};
// Produce an array that contains every item shared between all the
// passed-in arrays. (Aliased as "intersect" for back-compat.)
_.intersection = _.intersect = function(array) {
var rest = slice.call(arguments, 1);
return _.filter(_.uniq(array), function(item) {
return _.every(rest, function(other) {
return _.indexOf(other, item) >= 0;
});
});
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = function(array) {
var rest = _.flatten(slice.call(arguments, 1));
return _.filter(array, function(value){ return !_.include(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var results = new Array(length);
for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
return results;
};
// If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
// we need this function. Return the position of the first occurrence of an
// item in an array, or -1 if the item is not included in the array.
// Delegates to **ECMAScript 5**'s native `indexOf` if available.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i, l;
if (isSorted) {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
return -1;
};
// Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
_.lastIndexOf = function(array, item) {
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
while (i--) if (i in array && array[i] === item) return i;
return -1;
};
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (arguments.length <= 1) {
stop = start || 0;
start = 0;
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
while(idx < len) {
range[idx++] = start;
start += step;
}
return range;
};
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Binding with arguments is also known as `curry`.
// Delegates to **ECMAScript 5**'s native `Function.bind` if available.
// We check for `func.bind` first, to fail fast when `func` is undefined.
_.bind = function bind(func, context) {
var bound, args;
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
// Bind all of an object's methods to that object. Useful for ensuring that
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length == 0) funcs = _.functions(obj);
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memo = {};
hasher || (hasher = _.identity);
return function() {
var key = hasher.apply(this, arguments);
return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
};
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = function(func, wait) {
var args = slice.call(arguments, 2);
return setTimeout(function(){ return func.apply(func, args); }, wait);
};
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = function(func) {
return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, throttling, more;
var whenDone = _.debounce(function(){ more = throttling = false; }, wait);
return function() {
context = this; args = arguments;
var later = function() {
timeout = null;
if (more) func.apply(context, args);
whenDone();
};
if (!timeout) timeout = setTimeout(later, wait);
if (throttling) {
more = true;
} else {
func.apply(context, args);
}
whenDone();
throttling = true;
};
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
_.debounce = function(func, wait) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
// Returns a function that will be executed at most one time, no matter how
// often you call it. Useful for lazy initialization.
_.once = function(func) {
var ran = false, memo;
return function() {
if (ran) return memo;
ran = true;
return memo = func.apply(this, arguments);
};
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
var args = [func].concat(slice.call(arguments, 0));
return wrapper.apply(this, args);
};
};
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
};
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) { return func.apply(this, arguments); }
};
};
// Object Functions
// ----------------
// Retrieve the names of an object's properties.
// Delegates to **ECMAScript 5**'s native `Object.keys`
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
return _.map(obj, _.identity);
};
// Return a sorted list of the function names available on the object.
// Aliased as `methods`
_.functions = _.methods = function(obj) {
var names = [];
for (var key in obj) {
if (_.isFunction(obj[key])) names.push(key);
}
return names.sort();
};
// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
obj[prop] = source[prop];
}
});
return obj;
};
// Fill in a given object with default properties.
_.defaults = function(obj) {
each(slice.call(arguments, 1), function(source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
}
});
return obj;
};
// Create a (shallow-cloned) duplicate of an object.
_.clone = function(obj) {
if (!_.isObject(obj)) return obj;
return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
};
// Invokes interceptor with the obj, and then returns obj.
// The primary purpose of this method is to "tap into" a method chain, in
// order to perform operations on intermediate results within the chain.
_.tap = function(obj, interceptor) {
interceptor(obj);
return obj;
};
// Internal recursive comparison function.
function eq(a, b, stack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
// Unwrap any wrapped objects.
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// Invoke a custom `isEqual` method if one is provided.
if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = stack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (stack[length] == a) return true;
}
// Add the first object to the stack of traversed objects.
stack.push(a);
var size = 0, result = true;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
// Ensure commutative equality for sparse arrays.
if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;
}
}
} else {
// Objects with different constructors are not equivalent.
if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (_.has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
stack.pop();
return result;
}
// Perform a deep comparison to check if two objects are equal.
_.isEqual = function(a, b) {
return eq(a, b, []);
};
// Is a given array, string, or object empty?
// An "empty" object has no enumerable own-properties.
_.isEmpty = function(obj) {
if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
for (var key in obj) if (_.has(obj, key)) return false;
return true;
};
// Is a given value a DOM element?
_.isElement = function(obj) {
return !!(obj && obj.nodeType == 1);
};
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
_.isArray = nativeIsArray || function(obj) {
return toString.call(obj) == '[object Array]';
};
// Is a given variable an object?
_.isObject = function(obj) {
return obj === Object(obj);
};
// Is a given variable an arguments object?
_.isArguments = function(obj) {
return toString.call(obj) == '[object Arguments]';
};
if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && _.has(obj, 'callee'));
};
}
// Is a given value a function?
_.isFunction = function(obj) {
return toString.call(obj) == '[object Function]';
};
// Is a given value a string?
_.isString = function(obj) {
return toString.call(obj) == '[object String]';
};
// Is a given value a number?
_.isNumber = function(obj) {
return toString.call(obj) == '[object Number]';
};
// Is the given value `NaN`?
_.isNaN = function(obj) {
// `NaN` is the only value for which `===` is not reflexive.
return obj !== obj;
};
// Is a given value a boolean?
_.isBoolean = function(obj) {
return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
};
// Is a given value a date?
_.isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
// Is the given value a regular expression?
_.isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
// Is a given value equal to null?
_.isNull = function(obj) {
return obj === null;
};
// Is a given variable undefined?
_.isUndefined = function(obj) {
return obj === void 0;
};
// Has own property?
_.has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
// Utility Functions
// -----------------
// Run Underscore.js in *noConflict* mode, returning the `_` variable to its
// previous owner. Returns a reference to the Underscore object.
_.noConflict = function() {
root._ = previousUnderscore;
return this;
};
// Keep the identity function around for default iterators.
_.identity = function(value) {
return value;
};
// Run a function **n** times.
_.times = function (n, iterator, context) {
for (var i = 0; i < n; i++) iterator.call(context, i);
};
// Escape a string for HTML interpolation.
_.escape = function(string) {
return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\//g,'&#x2F;');
};
// Add your own custom functions to the Underscore object, ensuring that
// they're correctly added to the OOP wrapper as well.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
addToWrapper(name, _[name] = obj[name]);
});
};
// Generate a unique integer id (unique within the entire client session).
// Useful for temporary DOM ids.
var idCounter = 0;
_.uniqueId = function(prefix) {
var id = idCounter++;
return prefix ? prefix + id : id;
};
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /.^/;
// Within an interpolation, evaluation, or escaping, remove HTML escaping
// that had been previously added.
var unescape = function(code) {
return code.replace(/\\\\/g, '\\').replace(/\\'/g, "'");
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
_.template = function(str, data) {
var c = _.templateSettings;
var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(c.escape || noMatch, function(match, code) {
return "',_.escape(" + unescape(code) + "),'";
})
.replace(c.interpolate || noMatch, function(match, code) {
return "'," + unescape(code) + ",'";
})
.replace(c.evaluate || noMatch, function(match, code) {
return "');" + unescape(code).replace(/[\r\n\t]/g, ' ') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', '_', tmpl);
if (data) return func(data, _);
return function(data) {
return func.call(this, data, _);
};
};
// Add a "chain" function, which will delegate to the wrapper.
_.chain = function(obj) {
return _(obj).chain();
};
// The OOP Wrapper
// ---------------
// If Underscore is called as a function, it returns a wrapped object that
// can be used OO-style. This wrapper holds altered versions of all the
// underscore functions. Wrapped objects may be chained.
var wrapper = function(obj) { this._wrapped = obj; };
// Expose `wrapper.prototype` as `_.prototype`
_.prototype = wrapper.prototype;
// Helper function to continue chaining intermediate results.
var result = function(obj, chain) {
return chain ? _(obj).chain() : obj;
};
// A method to easily add functions to the OOP wrapper.
var addToWrapper = function(name, func) {
wrapper.prototype[name] = function() {
var args = slice.call(arguments);
unshift.call(args, this._wrapped);
return result(func.apply(_, args), this._chain);
};
};
// Add all of the Underscore functions to the wrapper object.
_.mixin(_);
// Add all mutator Array functions to the wrapper.
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
var wrapped = this._wrapped;
method.apply(wrapped, arguments);
var length = wrapped.length;
if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
return result(wrapped, this._chain);
};
});
// Add all accessor Array functions to the wrapper.
each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
return result(method.apply(this._wrapped, arguments), this._chain);
};
});
// Start chaining a wrapped Underscore object.
wrapper.prototype.chain = function() {
this._chain = true;
return this;
};
// Extracts the result from a wrapped and chained object.
wrapper.prototype.value = function() {
return this._wrapped;
};
}).call(this);

View File

@ -0,0 +1,32 @@
From: Michael Fladischer <FladischerMichael@fladi.at>
Date: Tue, 25 Jul 2017 22:06:20 +0200
Subject: Fix value of Language fields for django.po files.
---
django_filters/locale/fr/LC_MESSAGES/django.po | 2 +-
django_filters/locale/pl/LC_MESSAGES/django.po | 2 +-
django_filters/locale/zh_CN/LC_MESSAGES/django.po | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
--- a/django_filters/locale/fr/LC_MESSAGES/django.po
+++ b/django_filters/locale/fr/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: 2013-07-05 19:24+0200\n"
"Last-Translator: Axel Haustant <noirbizarre@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: French\n"
+"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
--- a/django_filters/locale/zh_CN/LC_MESSAGES/django.po
+++ b/django_filters/locale/zh_CN/LC_MESSAGES/django.po
@@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: 2016-01-30 17:50+0800\n"
"Last-Translator: Kane Blueriver <kxxoling@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
+"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

1
debian/pycompat vendored Normal file
View File

@ -0,0 +1 @@
2

View File

@ -0,0 +1,9 @@
Document: python-django-filter
Title: Python django-filter Documentation
Author: Alex Gaynor
Abstract: This documentation gives an introduction to django-filter.
Section: Programming/Python
Format: HTML
Index: /usr/share/doc/python-django-filters-doc/html/index.html
Files: /usr/share/doc/python-django-filters-doc/html/*.html

1
debian/python-django-filters-doc.docs vendored Normal file
View File

@ -0,0 +1 @@
docs/.build/html

1
debian/python-django-filters.docs vendored Normal file
View File

@ -0,0 +1 @@
README.rst

22
debian/rules vendored
View File

@ -1,5 +1,25 @@
#!/usr/bin/make -f
# -*- makefile -*-
export PYBUILD_NAME=django-filters
%:
dh $@ --with python2 --buildsystem=python_distutils
dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
.PHONY: override_dh_auto_build
override_dh_auto_build:
PYTHONPATH=. sphinx-build -b html -d docs/.build/.doctrees -N docs docs/.build/html
dh_auto_build
set -e; \
for loc in django_filters/locale/*; do \
test -r $$loc/LC_MESSAGES/django.po &&\
python3 setup.py compile_catalog --directory django_filters/locale/ --locale $$(basename $$loc) --domain django; \
done
.PHONY: override_dh_auto_test
override_dh_auto_test:
dh_auto_test -- --system=custom --test-args="{interpreter} ./runtests.py"
override_dh_clean:
rm -rf docs/.build
dh_clean

View File

@ -1 +0,0 @@
extend-diff-ignore="\.egg-info$"

3
debian/watch vendored Normal file
View File

@ -0,0 +1,3 @@
version=3
opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
http://pypi.debian.net/django-filter/django-filter-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))

View File

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

View File

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

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
django_filters

View File

@ -1,17 +1,19 @@
# flake8: noqa
from __future__ import absolute_import
import pkgutil
from .constants import STRICTNESS
from .filterset import FilterSet
from .filters import *
# We make the `rest_framework` module available without an additional import.
# If DRF is not installed we simply set None.
try:
# If DRF is not installed, no-op.
if pkgutil.find_loader('rest_framework') is not None:
from . import rest_framework
except ImportError:
rest_framework = None
del pkgutil
__version__ = '1.0.1'
__version__ = '1.1.0'
def parse_version(version):

View File

@ -3,7 +3,12 @@ from __future__ import absolute_import
import django
from django.conf import settings
from django.utils.timezone import make_aware as make_aware_orig
try:
from django.forms.utils import pretty_name
except ImportError: # Django 1.8
from django.forms.forms import pretty_name
# django-crispy-forms is optional
try:
@ -11,7 +16,9 @@ try:
except ImportError:
crispy_forms = None
is_crispy = 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
def is_crispy():
return 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms
# coreapi is optional (Note that uritemplate is a dependency of coreapi)
@ -22,6 +29,10 @@ try:
except ImportError:
coreapi = None
try:
import coreschema
except ImportError:
coreschema = None
def remote_field(field):
"""
@ -49,3 +60,12 @@ def format_value(widget, value):
if django.VERSION >= (1, 10):
return widget.format_value(value)
return widget._format_value(value)
def make_aware(value, timezone, is_dst):
"""is_dst was added for 1.9"""
if django.VERSION >= (1, 9):
return make_aware_orig(value, timezone, is_dst)
else:
return make_aware_orig(value, timezone)

View File

@ -1,4 +1,6 @@
from __future__ import absolute_import
from django.conf import settings as dj_settings
from django.core.signals import setting_changed
from django.utils.translation import ugettext_lazy as _
@ -6,7 +8,6 @@ from django.utils.translation import ugettext_lazy as _
from .constants import STRICTNESS
from .utils import deprecate
DEFAULTS = {
'DISABLE_HELP_TEXT': False,
'HELP_TEXT_FILTER': True,
@ -63,32 +64,30 @@ DEFAULTS = {
DEPRECATED_SETTINGS = [
'HELP_TEXT_FILTER',
'HELP_TEXT_EXCLUDE'
'HELP_TEXT_EXCLUDE',
]
def is_callable(value):
# check for callables, except types
return callable(value) and not isinstance(value, type)
class Settings(object):
def __init__(self):
for setting in DEFAULTS:
value = self.get_setting(setting)
setattr(self, setting, value)
def __getattr__(self, name):
if name not in DEFAULTS:
msg = "'%s' object has no attribute '%s'"
raise AttributeError(msg % (self.__class__.__name__, name))
def VERBOSE_LOOKUPS():
"""
VERBOSE_LOOKUPS accepts a dictionary of {terms: verbose expressions}
or a zero-argument callable that returns a dictionary.
"""
def fget(self):
if callable(self._VERBOSE_LOOKUPS):
self._VERBOSE_LOOKUPS = self._VERBOSE_LOOKUPS()
return self._VERBOSE_LOOKUPS
value = self.get_setting(name)
def fset(self, value):
self._VERBOSE_LOOKUPS = value
if is_callable(value):
value = value()
return locals()
VERBOSE_LOOKUPS = property(**VERBOSE_LOOKUPS())
# Cache the result
setattr(self, name, value)
return value
def get_setting(self, setting):
django_setting = 'FILTERS_%s' % setting
@ -107,9 +106,11 @@ class Settings(object):
if setting not in DEFAULTS:
return
# if exiting, refetch the value from settings.
value = value if enter else self.get_setting(setting)
setattr(self, setting, value)
# if exiting, delete value to repopulate
if enter:
setattr(self, setting, value)
else:
delattr(self, setting)
settings = Settings()

View File

@ -2,14 +2,17 @@
ALL_FIELDS = '__all__'
class STRICTNESS:
class IGNORE:
EMPTY_VALUES = ([], (), {}, '', None)
class STRICTNESS(object):
class IGNORE(object):
pass
class RETURN_NO_RESULTS:
class RETURN_NO_RESULTS(object):
pass
class RAISE_VALIDATION_ERROR:
class RAISE_VALIDATION_ERROR(object):
pass
# Values of False & True chosen for backward compatability reasons.

View File

@ -1,17 +1,17 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from datetime import datetime, time
from collections import namedtuple
from datetime import datetime, time
import django
from django import forms
from django.utils.dateparse import parse_datetime
from django.utils.encoding import force_str
from django.utils.translation import ugettext_lazy as _
from .conf import settings
from .utils import handle_timezone
from .widgets import RangeWidget, LookupTypeWidget, CSVWidget, BaseCSVWidget
from .widgets import BaseCSVWidget, CSVWidget, LookupTypeWidget, RangeWidget
class RangeField(forms.MultiValueField):
@ -43,10 +43,14 @@ class DateRangeField(RangeField):
start_date, stop_date = data_list
if start_date:
start_date = handle_timezone(
datetime.combine(start_date, time.min))
datetime.combine(start_date, time.min),
False
)
if stop_date:
stop_date = handle_timezone(
datetime.combine(stop_date, time.max))
datetime.combine(stop_date, time.max),
False
)
return slice(start_date, stop_date)
return None
@ -88,6 +92,7 @@ class LookupTypeField(forms.MultiValueField):
}
widget = LookupTypeWidget(**defaults)
kwargs['widget'] = widget
kwargs['help_text'] = field.help_text
super(LookupTypeField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
@ -176,3 +181,114 @@ class BaseRangeField(BaseCSVField):
code='invalid_values')
return value
class ChoiceIterator(object):
# Emulates the behavior of ModelChoiceIterator, but instead wraps
# the field's _choices iterable.
def __init__(self, field, choices):
self.field = field
self.choices = choices
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for choice in self.choices:
yield choice
def __len__(self):
add = 1 if self.field.empty_label is not None else 0
add += 1 if self.field.null_label is not None else 0
return len(self.choices) + add
class ModelChoiceIterator(forms.models.ModelChoiceIterator):
# Extends the base ModelChoiceIterator to add in 'null' choice handling.
# This is a bit verbose since we have to insert the null choice after the
# empty choice, but before the remainder of the choices.
def __iter__(self):
iterable = super(ModelChoiceIterator, self).__iter__()
if self.field.empty_label is not None:
yield next(iterable)
if self.field.null_label is not None:
yield (self.field.null_value, self.field.null_label)
# Python 2 lacks 'yield from'
for value in iterable:
yield value
def __len__(self):
add = 1 if self.field.null_label is not None else 0
return super(ModelChoiceIterator, self).__len__() + add
class ChoiceIteratorMixin(object):
def __init__(self, *args, **kwargs):
self.null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
self.null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
super(ChoiceIteratorMixin, self).__init__(*args, **kwargs)
def _get_choices(self):
if django.VERSION >= (1, 11):
return super(ChoiceIteratorMixin, self)._get_choices()
# HACK: Django < 1.11 does not allow a custom iterator to be provided.
# This code only executes for Model*ChoiceFields.
if hasattr(self, '_choices'):
return self._choices
return self.iterator(self)
def _set_choices(self, value):
super(ChoiceIteratorMixin, self)._set_choices(value)
value = self.iterator(self, self._choices)
self._choices = self.widget.choices = value
choices = property(_get_choices, _set_choices)
# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label
class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
super(ChoiceField, self).__init__(*args, **kwargs)
class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField):
iterator = ChoiceIterator
def __init__(self, *args, **kwargs):
self.empty_label = None
super(MultipleChoiceField, self).__init__(*args, **kwargs)
class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField):
iterator = ModelChoiceIterator
def to_python(self, value):
# bypass the queryset value check
if self.null_label is not None and value == self.null_value:
return value
return super(ModelChoiceField, self).to_python(value)
class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField):
iterator = ModelChoiceIterator
def _check_values(self, value):
null = self.null_label is not None and value and self.null_value in value
if null: # remove the null value and any potential duplicates
value = [v for v in value if v != self.null_value]
result = list(super(ModelMultipleChoiceField, self)._check_values(value))
result += [self.null_value] if null else []
return result

View File

@ -1,25 +1,36 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from datetime import timedelta
from django import forms
from django.db.models import Q
from django.db.models.sql.constants import QUERY_TERMS
from django.db.models.constants import LOOKUP_SEP
from django.db.models.sql.constants import QUERY_TERMS
from django.utils import six
from django.utils.itercompat import is_iterable
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from .compat import pretty_name
from .conf import settings
from .constants import EMPTY_VALUES
from .fields import (
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
BaseCSVField,
BaseRangeField,
ChoiceField,
DateRangeField,
DateTimeRangeField,
IsoDateTimeField,
Lookup,
LookupTypeField,
ModelChoiceField,
ModelMultipleChoiceField,
MultipleChoiceField,
RangeField,
TimeRangeField
)
from .utils import label_for_filter, pretty_name
from .utils import deprecate, label_for_filter
__all__ = [
'AllValuesFilter',
@ -56,26 +67,40 @@ __all__ = [
LOOKUP_TYPES = sorted(QUERY_TERMS)
EMPTY_VALUES = ([], (), {}, '', None)
def _extra_attr(attr):
fmt = ("The `.%s` attribute has been deprecated in favor of making it accessible "
"alongside the other field kwargs. You should now access it as `.extra['%s']`.")
def fget(self):
deprecate(fmt % (attr, attr))
return self.extra.get(attr)
def fset(self, value):
deprecate(fmt % (attr, attr))
self.extra[attr] = value
return {'fget': fget, 'fset': fset}
class Filter(object):
creation_counter = 0
field_class = forms.Field
def __init__(self, name=None, label=None, widget=None, method=None, lookup_expr='exact',
required=False, distinct=False, exclude=False, **kwargs):
self.name = name
def __init__(self, field_name=None, label=None, method=None, lookup_expr='exact',
distinct=False, exclude=False, **kwargs):
self.field_name = field_name
if field_name is None and 'name' in kwargs:
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
self.field_name = kwargs.pop('name')
self.label = label
self.method = method
self.lookup_expr = lookup_expr
self.widget = widget
self.required = required
self.extra = kwargs
self.distinct = distinct
self.exclude = exclude
self.extra = kwargs
self.extra.setdefault('required', False)
self.creation_counter = Filter.creation_counter
Filter.creation_counter += 1
@ -107,12 +132,24 @@ class Filter(object):
return locals()
method = property(**method())
def name():
def fget(self):
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
return self.field_name
def fset(self, value):
deprecate("`Filter.name` has been renamed to `Filter.field_name`.")
self.field_name = value
return locals()
name = property(**name())
def label():
def fget(self):
if self._label is None and hasattr(self, 'parent'):
model = self.parent._meta.model
self._label = label_for_filter(
model, self.name, self.lookup_expr, self.exclude
model, self.field_name, self.lookup_expr, self.exclude
)
return self._label
@ -122,6 +159,10 @@ class Filter(object):
return locals()
label = property(**label())
# deprecated field props
widget = property(**_extra_attr('widget'))
required = property(**_extra_attr('required'))
@property
def field(self):
if not hasattr(self, '_field'):
@ -151,13 +192,11 @@ class Filter(object):
if x in self.lookup_expr:
lookup.append(choice)
self._field = LookupTypeField(self.field_class(
required=self.required, widget=self.widget, **field_kwargs),
lookup, required=self.required, label=self.label)
self._field = LookupTypeField(
self.field_class(**field_kwargs), lookup,
required=field_kwargs['required'], label=self.label)
else:
self._field = self.field_class(required=self.required,
label=self.label, widget=self.widget,
**field_kwargs)
self._field = self.field_class(label=self.label, **field_kwargs)
return self._field
def filter(self, qs, value):
@ -170,7 +209,7 @@ class Filter(object):
return qs
if self.distinct:
qs = qs.distinct()
qs = self.get_method(qs)(**{'%s__%s' % (self.name, lookup): value})
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, lookup): value})
return qs
@ -183,39 +222,17 @@ class BooleanFilter(Filter):
class ChoiceFilter(Filter):
field_class = forms.ChoiceField
field_class = ChoiceField
def __init__(self, *args, **kwargs):
empty_label = kwargs.pop('empty_label', settings.EMPTY_CHOICE_LABEL)
null_label = kwargs.pop('null_label', settings.NULL_CHOICE_LABEL)
null_value = kwargs.pop('null_value', settings.NULL_CHOICE_VALUE)
self.null_value = null_value
if 'choices' in kwargs:
choices = kwargs.get('choices')
# coerce choices to list
if callable(choices):
choices = choices()
choices = list(choices)
# create the empty/null choices that prepend the original choices
prepend = []
if empty_label is not None:
prepend.append(('', empty_label))
if null_label is not None:
prepend.append((null_value, null_label))
kwargs['choices'] = prepend + choices
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
super(ChoiceFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value != self.null_value:
return super(ChoiceFilter, self).filter(qs, value)
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): None})
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): None})
return qs.distinct() if self.distinct else qs
@ -250,13 +267,14 @@ class MultipleChoiceFilter(Filter):
`distinct` defaults to `True` as to-many relationships will generally
require this.
"""
field_class = forms.MultipleChoiceField
field_class = MultipleChoiceField
always_filter = True
def __init__(self, *args, **kwargs):
kwargs.setdefault('distinct', True)
self.conjoined = kwargs.pop('conjoined', False)
self.null_value = kwargs.get('null_value', settings.NULL_CHOICE_VALUE)
super(MultipleChoiceFilter, self).__init__(*args, **kwargs)
def is_noop(self, qs, value):
@ -268,7 +286,7 @@ class MultipleChoiceFilter(Filter):
return False
# A reasonable default for being a noop...
if self.required and len(value) == len(self.field.choices):
if self.extra.get('required') and len(value) == len(self.field.choices):
return True
return False
@ -284,6 +302,8 @@ class MultipleChoiceFilter(Filter):
if not self.conjoined:
q = Q()
for v in set(value):
if v == self.null_value:
v = None
predicate = self.get_filter_predicate(v)
if self.conjoined:
qs = self.get_method(qs)(**predicate)
@ -297,9 +317,9 @@ class MultipleChoiceFilter(Filter):
def get_filter_predicate(self, v):
try:
return {self.name: getattr(v, self.field.to_field_name)}
return {self.field_name: getattr(v, self.field.to_field_name)}
except (AttributeError, TypeError):
return {self.name: v}
return {self.field_name: v}
class TypedMultipleChoiceFilter(MultipleChoiceFilter):
@ -386,12 +406,16 @@ class QuerySetRequestMixin(object):
return super(QuerySetRequestMixin, self).field
class ModelChoiceFilter(QuerySetRequestMixin, Filter):
field_class = forms.ModelChoiceField
class ModelChoiceFilter(QuerySetRequestMixin, ChoiceFilter):
field_class = ModelChoiceField
def __init__(self, *args, **kwargs):
kwargs.setdefault('empty_label', settings.EMPTY_CHOICE_LABEL)
super(ModelChoiceFilter, self).__init__(*args, **kwargs)
class ModelMultipleChoiceFilter(QuerySetRequestMixin, MultipleChoiceFilter):
field_class = forms.ModelMultipleChoiceField
field_class = ModelMultipleChoiceField
class NumberFilter(Filter):
@ -404,13 +428,13 @@ class NumericRangeFilter(Filter):
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__%s' % (self.name, self.lookup_expr)
lookup = '%s__%s' % (self.field_name, self.lookup_expr)
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__startswith' % self.name: value.start})
qs = self.get_method(qs)(**{'%s__startswith' % self.field_name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__endswith' % self.name: value.stop})
qs = self.get_method(qs)(**{'%s__endswith' % self.field_name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
@ -422,13 +446,13 @@ class RangeFilter(Filter):
def filter(self, qs, value):
if value:
if value.start is not None and value.stop is not None:
lookup = '%s__range' % self.name
lookup = '%s__range' % self.field_name
return self.get_method(qs)(**{lookup: (value.start, value.stop)})
else:
if value.start is not None:
qs = self.get_method(qs)(**{'%s__gte' % self.name: value.start})
qs = self.get_method(qs)(**{'%s__gte' % self.field_name: value.start})
if value.stop is not None:
qs = self.get_method(qs)(**{'%s__lte' % self.name: value.stop})
qs = self.get_method(qs)(**{'%s__lte' % self.field_name: value.stop})
if self.distinct:
qs = qs.distinct()
return qs
@ -480,7 +504,7 @@ class DateRangeFilter(ChoiceFilter):
value = ''
assert value in self.options
qs = self.options[value][1](qs, self.name)
qs = self.options[value][1](qs, self.field_name)
if self.distinct:
qs = qs.distinct()
return qs
@ -502,7 +526,7 @@ class AllValuesFilter(ChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.name).values_list(self.name, flat=True)
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesFilter, self).field
@ -511,7 +535,7 @@ class AllValuesMultipleFilter(MultipleChoiceFilter):
@property
def field(self):
qs = self.model._default_manager.distinct()
qs = qs.order_by(self.name).values_list(self.name, flat=True)
qs = qs.order_by(self.field_name).values_list(self.field_name, flat=True)
self.extra['choices'] = [(o, o) for o in qs]
return super(AllValuesMultipleFilter, self).field
@ -619,6 +643,7 @@ class OrderingFilter(BaseCSVFilter, ChoiceFilter):
kwargs['choices'] = self.build_choices(fields, field_labels)
kwargs.setdefault('label', _('Ordering'))
kwargs.setdefault('help_text', '')
kwargs.setdefault('null_label', None)
super(OrderingFilter, self).__init__(*args, **kwargs)
@ -685,7 +710,7 @@ class FilterMethod(object):
if value in EMPTY_VALUES:
return qs
return self.method(qs, self.f.name, value)
return self.method(qs, self.f.field_name, value)
@property
def method(self):
@ -701,7 +726,7 @@ class FilterMethod(object):
# otherwise, method is the name of a method on the parent FilterSet.
assert hasattr(instance, 'parent'), \
"Filter '%s' must have a parent FilterSet to find '.%s()'" % \
(instance.name, instance.method)
(instance.field_name, instance.method)
parent = instance.parent
method = getattr(parent, instance.method, None)

View File

@ -1,63 +1,66 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
import copy
from collections import OrderedDict
from django import forms
from django.forms.forms import NON_FIELD_ERRORS
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.related import ForeignObjectRel
from django.utils import six
from .conf import settings
from .compat import remote_field, remote_queryset
from .constants import ALL_FIELDS, STRICTNESS
from .filters import (Filter, CharFilter, BooleanFilter, BaseInFilter, BaseRangeFilter,
ChoiceFilter, DateFilter, DateTimeFilter, TimeFilter, ModelChoiceFilter,
ModelMultipleChoiceFilter, NumberFilter, UUIDFilter, DurationFilter)
from .utils import try_dbfield, get_all_model_fields, get_model_field, resolve_field
from .conf import settings
from .constants import ALL_FIELDS, EMPTY_VALUES, STRICTNESS
from .filters import (
BaseInFilter,
BaseRangeFilter,
BooleanFilter,
CharFilter,
ChoiceFilter,
DateFilter,
DateTimeFilter,
DurationFilter,
Filter,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
NumberFilter,
TimeFilter,
UUIDFilter
)
from .utils import (
deprecate,
get_all_model_fields,
get_model_field,
resolve_field,
try_dbfield
)
def get_filter_name(field_name, lookup_expr):
"""
Combine a field name and lookup expression into a usable filter name.
Exact lookups are the implicit default, so "exact" is stripped from the
end of the filter name.
"""
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
def _together_valid(form, fieldset):
field_presence = [
form.cleaned_data.get(field) not in EMPTY_VALUES
for field in fieldset
]
# This also works with transformed exact lookups, such as 'date__exact'
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
return filter_name
if any(field_presence):
return all(field_presence)
return True
def get_full_clean_override(together):
# coerce together to list of pairs
if isinstance(together[0], (six.string_types)):
together = [together]
def full_clean(form):
def add_error(message):
try:
form.add_error(None, message)
except AttributeError:
form._errors[NON_FIELD_ERRORS] = message
def all_valid(fieldset):
cleaned_data = form.cleaned_data
count = len([i for i in fieldset if cleaned_data.get(i)])
return 0 < count < len(fieldset)
super(form.__class__, form).full_clean()
message = 'Following fields must be together: %s'
if isinstance(together[0], (list, tuple)):
for each in together:
if all_valid(each):
return add_error(message % ','.join(each))
elif all_valid(together):
return add_error(message % ','.join(together))
for each in together:
if not _together_valid(form, each):
return form.add_error(None, message % ','.join(each))
return full_clean
@ -73,6 +76,8 @@ class FilterSetOptions(object):
self.form = getattr(options, 'form', forms.Form)
if hasattr(options, 'together'):
deprecate('The `Meta.together` option has been deprecated in favor of overriding `Form.clean`.', 1)
self.together = getattr(options, 'together', None)
@ -94,17 +99,21 @@ class FilterSetMetaclass(type):
if isinstance(obj, Filter)
]
# Default the `filter.name` to the attribute name on the filterset
# Default the `filter.field_name` to the attribute name on the filterset
for filter_name, f in filters:
if getattr(f, 'name', None) is None:
f.name = filter_name
if getattr(f, 'field_name', None) is None:
f.field_name = filter_name
filters.sort(key=lambda x: x[1].creation_counter)
# merge declared filters from base classes
for base in reversed(bases):
if hasattr(base, 'declared_filters'):
filters = list(base.declared_filters.items()) + filters
filters = [
(name, f) for name, f
in base.declared_filters.items()
if name not in attrs
] + filters
return OrderedDict(filters)
@ -137,6 +146,7 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': remote_field(f).field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ForeignKey: {
@ -144,6 +154,7 @@ FILTER_FOR_DBFIELD_DEFAULTS = {
'extra': lambda f: {
'queryset': remote_queryset(f),
'to_field_name': remote_field(f).field_name,
'null_label': settings.NULL_CHOICE_LABEL if f.null else None,
}
},
models.ManyToManyField: {
@ -261,6 +272,22 @@ class BaseFilterSet(object):
return OrderedDict(fields)
@classmethod
def get_filter_name(cls, field_name, lookup_expr):
"""
Combine a field name and lookup expression into a usable filter name.
Exact lookups are the implicit default, so "exact" is stripped from the
end of the filter name.
"""
filter_name = LOOKUP_SEP.join([field_name, lookup_expr])
# This also works with transformed exact lookups, such as 'date__exact'
_exact = LOOKUP_SEP + 'exact'
if filter_name.endswith(_exact):
filter_name = filter_name[:-len(_exact)]
return filter_name
@classmethod
def get_filters(cls):
"""
@ -290,7 +317,7 @@ class BaseFilterSet(object):
continue
for lookup_expr in lookups:
filter_name = get_filter_name(field_name, lookup_expr)
filter_name = cls.get_filter_name(field_name, lookup_expr)
# If the filter is explicitly declared on the class, skip generation
if filter_name in cls.declared_filters:
@ -314,11 +341,11 @@ class BaseFilterSet(object):
return filters
@classmethod
def filter_for_field(cls, f, name, lookup_expr='exact'):
def filter_for_field(cls, f, field_name, lookup_expr='exact'):
f, lookup_type = resolve_field(f, lookup_expr)
default = {
'name': name,
'field_name': field_name,
'lookup_expr': lookup_expr,
}
@ -329,16 +356,16 @@ class BaseFilterSet(object):
"%s resolved field '%s' with '%s' lookup to an unrecognized field "
"type %s. Try adding an override to 'Meta.filter_overrides'. See: "
"https://django-filter.readthedocs.io/en/develop/ref/filterset.html#customise-filter-generation-with-filter-overrides"
) % (cls.__name__, name, lookup_expr, f.__class__.__name__)
) % (cls.__name__, field_name, lookup_expr, f.__class__.__name__)
return filter_class(**default)
@classmethod
def filter_for_reverse_field(cls, f, name):
def filter_for_reverse_field(cls, f, field_name):
rel = remote_field(f.field)
queryset = f.field.model._default_manager.all()
default = {
'name': name,
'field_name': field_name,
'queryset': queryset,
}
if rel.multiple:
@ -421,8 +448,8 @@ class FilterSet(six.with_metaclass(FilterSetMetaclass, BaseFilterSet)):
pass
def filterset_factory(model):
meta = type(str('Meta'), (object,), {'model': model, 'fields': ALL_FIELDS})
def filterset_factory(model, fields=ALL_FIELDS):
meta = type(str('Meta'), (object,), {'model': model, 'fields': fields})
filterset = type(str('%sFilterSet' % model._meta.object_name),
(FilterSet,), {'Meta': meta})
return filterset

Binary file not shown.

View File

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

View File

@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: 2013-07-05 19:24+0200\n"
"Last-Translator: Axel Haustant <noirbizarre@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: French\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

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

View File

@ -12,7 +12,7 @@ msgstr ""
"PO-Revision-Date: 2016-01-30 17:50+0800\n"
"Last-Translator: Kane Blueriver <kxxoling@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

View File

@ -1,46 +1,23 @@
from __future__ import absolute_import
from django.template import Template, TemplateDoesNotExist, loader
import warnings
from django.template import loader
from django.utils import six
from rest_framework.compat import template_render
from rest_framework.filters import BaseFilterBackend
from . import filters, filterset
from .. import compat
from . import filterset
CRISPY_TEMPLATE = """
{% load crispy_forms_tags %}
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}
"""
FILTER_TEMPLATE = """
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
<form class="form" action="" method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>
"""
if compat.is_crispy:
template_path = 'django_filters/rest_framework/crispy_form.html'
template_default = CRISPY_TEMPLATE
else:
template_path = 'django_filters/rest_framework/form.html'
template_default = FILTER_TEMPLATE
class DjangoFilterBackend(BaseFilterBackend):
class DjangoFilterBackend(object):
default_filter_set = filterset.FilterSet
template = template_path
@property
def template(self):
if compat.is_crispy():
return 'django_filters/rest_framework/crispy_form.html'
return 'django_filters/rest_framework/form.html'
def get_filter_class(self, view, queryset=None):
"""
@ -59,8 +36,10 @@ class DjangoFilterBackend(BaseFilterBackend):
return filter_class
if filter_fields:
MetaBase = getattr(self.default_filter_set, 'Meta', object)
class AutoFilterSet(self.default_filter_set):
class Meta:
class Meta(MetaBase):
model = queryset.model
fields = filter_fields
@ -82,24 +61,44 @@ class DjangoFilterBackend(BaseFilterBackend):
return None
filter_instance = filter_class(request.query_params, queryset=queryset, request=request)
try:
template = loader.get_template(self.template)
except TemplateDoesNotExist:
template = Template(template_default)
return template_render(template, context={
template = loader.get_template(self.template)
context = {
'filter': filter_instance
})
}
return template.render(context, request)
def get_coreschema_field(self, field):
if isinstance(field, filters.NumberFilter):
field_cls = compat.coreschema.Number
else:
field_cls = compat.coreschema.String
return field_cls(
description=six.text_type(field.extra.get('help_text', ''))
)
def get_schema_fields(self, view):
# This is not compatible with widgets where the query param differs from the
# filter's attribute name. Notably, this includes `MultiWidget`, where query
# params will be of the format `<name>_0`, `<name>_1`, etc...
assert compat.coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
filter_class = self.get_filter_class(view, view.get_queryset())
assert compat.coreschema is not None, 'coreschema must be installed to use `get_schema_fields()`'
filter_class = getattr(view, 'filter_class', None)
if filter_class is None:
try:
filter_class = self.get_filter_class(view, view.get_queryset())
except Exception:
warnings.warn(
"{} is not compatible with schema generation".format(view.__class__)
)
filter_class = None
return [] if not filter_class else [
compat.coreapi.Field(
name=field_name, required=False, location='query', description=six.text_type(field.field.help_text))
for field_name, field in filter_class().filters.items()
name=field_name,
required=field.extra['required'],
location='query',
schema=self.get_coreschema_field(field)
) for field_name, field in filter_class.base_filters.items()
]

View File

@ -1,18 +1,16 @@
from __future__ import absolute_import
from copy import deepcopy
from django import forms
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django_filters import filterset
from .. import compat, utils
from .filters import BooleanFilter, IsoDateTimeFilter
from .. import compat
if compat.is_crispy:
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
FILTER_FOR_DBFIELD_DEFAULTS.update({
@ -24,11 +22,15 @@ FILTER_FOR_DBFIELD_DEFAULTS.update({
class FilterSet(filterset.FilterSet):
FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
@property
def form(self):
form = super(FilterSet, self).form
if compat.is_crispy:
layout_components = list(self.form.fields.keys()) + [
if compat.is_crispy():
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
layout_components = list(form.fields.keys()) + [
Submit('', _('Submit'), css_class='btn-default'),
]
helper = FormHelper()
@ -36,4 +38,15 @@ class FilterSet(filterset.FilterSet):
helper.template_pack = 'bootstrap3'
helper.layout = Layout(*layout_components)
self.form.helper = helper
form.helper = helper
return form
@property
def qs(self):
from rest_framework.exceptions import ValidationError
try:
return super(FilterSet, self).qs
except forms.ValidationError as e:
raise ValidationError(utils.raw_validation(e))

View File

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

View File

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

View File

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

View File

@ -1,22 +1,20 @@
import warnings
import django
from django.conf import settings
from django.core.exceptions import FieldError
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.expressions import Expression
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import RelatedField, ForeignObjectRel
from django.db.models.fields.related import ForeignObjectRel, RelatedField
from django.forms import ValidationError
from django.utils import six, timezone
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext as _
try:
from django.forms.utils import pretty_name
except ImportError: # Django 1.8
from django.forms.forms import pretty_name
from .compat import remote_field, remote_model
from .compat import make_aware, remote_field, remote_model
from .exceptions import FieldLookupError
@ -124,6 +122,10 @@ def resolve_field(model_field, lookup_expr):
try:
while lookups:
name = lookups[0]
args = (lhs, name)
if django.VERSION < (2, 0):
# rest_of_lookups was removed in Django 2.0
args += (lookups,)
# If there is just one part left, try first get_lookup() so
# that if the lhs supports both transform and lookup for the
# name, then lookup will be picked.
@ -133,20 +135,20 @@ def resolve_field(model_field, lookup_expr):
# We didn't find a lookup. We are going to interpret
# the name as transform, and do an Exact lookup against
# it.
lhs = query.try_transform(lhs, name, lookups)
lhs = query.try_transform(*args)
final_lookup = lhs.get_lookup('exact')
return lhs.output_field, final_lookup.lookup_name
lhs = query.try_transform(lhs, name, lookups)
lhs = query.try_transform(*args)
lookups = lookups[1:]
except FieldError as e:
six.raise_from(FieldLookupError(model_field, lookup_expr), e)
def handle_timezone(value):
def handle_timezone(value, is_dst=None):
if settings.USE_TZ and timezone.is_naive(value):
return timezone.make_aware(value, timezone.get_default_timezone())
return make_aware(value, timezone.get_current_timezone(), is_dst)
elif not settings.USE_TZ and timezone.is_aware(value):
return timezone.make_naive(value, timezone.UTC())
return timezone.make_naive(value, timezone.utc)
return value
@ -172,7 +174,10 @@ def verbose_field_name(model, field_name):
names = []
for part in parts:
if isinstance(part, ForeignObjectRel):
names.append(force_text(part.related_name))
if part.related_name:
names.append(part.related_name.replace('_', ' '))
else:
return '[invalid name]'
else:
names.append(force_text(part.verbose_name))
@ -224,6 +229,27 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False):
verbose_expression += [verbose_lookup_expr(lookup_expr)]
verbose_expression = [force_text(part) for part in verbose_expression if part]
verbose_expression = pretty_name(' '.join(verbose_expression))
verbose_expression = capfirst(' '.join(verbose_expression))
return verbose_expression
def raw_validation(error):
"""
Deconstruct a django.forms.ValidationError into a primitive structure
eg, plain dicts and lists.
"""
if isinstance(error, ValidationError):
if hasattr(error, 'error_dict'):
error = error.error_dict
elif not hasattr(error, 'message'):
error = error.error_list
else:
error = error.message
if isinstance(error, dict):
return {key: raw_validation(value) for key, value in error.items()}
elif isinstance(error, list):
return [raw_validation(value) for value in error]
else:
return error

View File

@ -1,9 +1,13 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.views.generic import View
from django.views.generic.list import MultipleObjectMixin
from django.views.generic.list import MultipleObjectTemplateResponseMixin
from django.views.generic.list import (
MultipleObjectMixin,
MultipleObjectTemplateResponseMixin
)
from .constants import ALL_FIELDS
from .filterset import filterset_factory
@ -12,6 +16,7 @@ class FilterMixin(object):
A mixin that provides a way to show and handle a FilterSet in a request.
"""
filterset_class = None
filter_fields = ALL_FIELDS
def get_filterset_class(self):
"""
@ -20,7 +25,7 @@ class FilterMixin(object):
if self.filterset_class:
return self.filterset_class
elif self.model:
return filterset_factory(self.model)
return filterset_factory(model=self.model, fields=self.filter_fields)
else:
msg = "'%s' must define 'filterset_class' or 'model'"
raise ImproperlyConfigured(msg % self.__class__.__name__)

View File

@ -1,18 +1,16 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from collections import Iterable
from itertools import chain
try:
from urllib.parse import urlencode
except:
from urllib import urlencode # noqa
from re import search, sub
import django
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms.widgets import flatatt
from django.forms.utils import flatatt
from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_text
from django.utils.http import urlencode
from django.utils.safestring import mark_safe
from django.utils.six import string_types
from django.utils.translation import ugettext as _
@ -36,7 +34,10 @@ class LinkWidget(forms.Widget):
self.data = {}
if value is None:
value = ''
final_attrs = self.build_attrs(attrs)
if django.VERSION < (1, 11):
final_attrs = self.build_attrs(attrs)
else:
final_attrs = self.build_attrs(self.attrs, extra_attrs=attrs)
output = ['<ul%s>' % flatatt(final_attrs)]
options = self.render_options(choices, [value], name)
if options:
@ -80,19 +81,81 @@ class LinkWidget(forms.Widget):
return '<li><a%(attrs)s href="?%(query_string)s">%(label)s</a></li>'
class SuffixedMultiWidget(forms.MultiWidget):
"""
A MultiWidget that allows users to provide custom suffixes instead of indexes.
- Suffixes must be unique.
- There must be the same number of suffixes as fields.
"""
suffixes = []
def __init__(self, *args, **kwargs):
super(SuffixedMultiWidget, self).__init__(*args, **kwargs)
assert len(self.widgets) == len(self.suffixes)
assert len(self.suffixes) == len(set(self.suffixes))
def suffixed(self, name, suffix):
return '_'.join([name, suffix]) if suffix else name
def get_context(self, name, value, attrs):
context = super(SuffixedMultiWidget, self).get_context(name, value, attrs)
for subcontext, suffix in zip(context['widget']['subwidgets'], self.suffixes):
subcontext['name'] = self.suffixed(name, suffix)
return context
def value_from_datadict(self, data, files, name):
return [
widget.value_from_datadict(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
]
def value_omitted_from_data(self, data, files, name):
return all(
widget.value_omitted_from_data(data, files, self.suffixed(name, suffix))
for widget, suffix in zip(self.widgets, self.suffixes)
)
# Django < 1.11 compat
def format_output(self, rendered_widgets):
rendered_widgets = [
self.replace_name(output, i)
for i, output in enumerate(rendered_widgets)
]
return '\n'.join(rendered_widgets)
def replace_name(self, output, index):
result = search(r'name="(?P<name>.*)_%d"' % index, output)
name = result.group('name')
name = self.suffixed(name, self.suffixes[index])
name = 'name="%s"' % name
return sub(r'name=".*_%d"' % index, name, output)
def decompress(self, value):
if value is None:
return [None, None]
return value
class RangeWidget(forms.MultiWidget):
template_name = 'django_filters/widgets/multiwidget.html'
def __init__(self, attrs=None):
widgets = (forms.TextInput, forms.TextInput)
super(RangeWidget, self).__init__(widgets, attrs)
def format_output(self, rendered_widgets):
# Method was removed in Django 1.11.
return '-'.join(rendered_widgets)
def decompress(self, value):
if value:
return [value.start, value.stop]
return [None, None]
def format_output(self, rendered_widgets):
return '-'.join(rendered_widgets)
class LookupTypeWidget(forms.MultiWidget):
def decompress(self, value):
@ -158,7 +221,7 @@ class BaseCSVWidget(forms.Widget):
if len(value) <= 1:
# delegate to main widget (Select, etc...) if not multiple values
value = value[0] if value else value
value = value[0] if value else ''
return super(BaseCSVWidget, self).render(name, value, attrs)
# if we have multiple values, we need to force render as a text input
@ -187,22 +250,19 @@ class QueryArrayWidget(BaseCSVWidget, forms.TextInput):
def value_from_datadict(self, data, files, name):
if not isinstance(data, MultiValueDict):
for key, value in data.items():
# treat value as csv string: ?foo=1,2
if isinstance(value, string_types):
data[key] = [x.strip() for x in value.rstrip(',').split(',') if x]
data = MultiValueDict(data)
values_list = data.getlist(name, data.getlist('%s[]' % name)) or []
if isinstance(values_list, string_types):
values_list = [values_list]
# apparently its an array, so no need to process it's values as csv
# ?foo=1&foo=2 -> data.getlist(foo) -> foo = [1, 2]
# ?foo[]=1&foo[]=2 -> data.getlist(foo[]) -> foo = [1, 2]
if len(values_list) > 1:
if len(values_list) > 0:
ret = [x for x in values_list if x]
elif len(values_list) == 1:
# treat first element as csv string
# ?foo=1,2 -> data.getlist(foo) -> foo = ['1,2']
ret = [x.strip() for x in values_list[0].rstrip(',').split(',') if x]
else:
ret = []

BIN
docs/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -48,9 +48,9 @@ copyright = u'2013, Alex Gaynor and others.'
# built documents.
#
# The short X.Y version.
version = '1.0.1'
version = '1.1.0'
# The full version, including alpha/beta/rc tags.
release = '1.0.1'
release = '1.1.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -65,3 +65,26 @@ supported versions of Python and Django. Install tox, and then simply run:
$ pip install tox
$ tox
Housekeeping
------------
The ``isort`` utility is used to maintain module imports. You can either test
the module imports with the appropriate `tox` env, or with `isort` directly.
.. code-block:: bash
$ pip install tox
$ tox -e isort
# or
$ pip install isort
$ isort --check-only --recursive django_filters tests
To sort the imports, simply remove the ``--check-only`` option.
.. code-block:: bash
$ isort --recursive django_filters tests

View File

@ -10,24 +10,25 @@ Django-filter can be installed from PyPI with tools like ``pip``:
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
.. note::
.. code-block:: python
django-filter provides *some* localization for *some* languages. If you do
not need these translations (or would rather provide your own), then it is
unnecessary to add django-filter to the ``INSTALLED_APPS`` setting.
INSTALLED_APPS = [
...
'django_filters',
]
Requirements
------------
Django-filter is tested against all supported versions of Python and `Django`__,
as well as the latest versions of Django REST Framework (`DRF`__).
as well as the latest version of Django REST Framework (`DRF`__).
__ https://www.djangoproject.com/download/
__ http://www.django-rest-framework.org/
* **Python**: 2.7, 3.3, 3.4, 3.5
* **Django**: 1.8, 1.9, 1.10
* **DRF**: 3.4, 3.5
* **Python**: 2.7, 3.4, 3.5, 3.6
* **Django**: 1.10, 1.11
* **DRF**: 3.7

View File

@ -1,6 +1,40 @@
================
===============
Migration Guide
===============
----------------
Migrating to 2.0
----------------
Removal of the ``Meta.together`` option
---------------------------------------
The ``Meta.together`` has been deprecated in favor of userland implementations
that override the ``clean`` method of the ``Meta.form`` class. An example will
be provided in a "recipes" secion in future docs.
``Filter.name`` renamed to ``Filter.field_name``
------------------------------------------------
The filter ``name`` has been renamed to ``field_name`` as a way to disambiguate
the filter's attribute name on its FilterSet class from the ``field_name`` used
for filtering purposes.
FilterSet strictness has been removed
-------------------------------------
Strictness handling has been removed from the ``FilterSet`` and added to the
view layer. As a result, the ``FILTERS_STRICTNESS`` setting, ``Meta.strict``
option, and ``strict`` argument for the ``FilterSet`` initializer have all
been removed.
To alter strictness behavior, the appropriate view code should be overridden.
More details will be provided in future docs.
----------------
Migrating to 1.0
================
----------------
The 1.0 release of django-filter introduces several API changes and refinements
that break forwards compatibility. Below is a list of deprecations and
@ -8,6 +42,29 @@ instructions on how to migrate to the 1.0 release. A forwards-compatible 0.15
release has also been created to help with migration. It is compatible with
both the existing and new APIs and will raise warnings for deprecated behavior.
Enabling warnings
-----------------
To view the deprecations, you may need to enable warnings within python. This
can be achieved with either the ``-W`` `flag`__, or with ``PYTHONWARNINGS``
`environment variable`__. For example, you could run your test suite like so:
.. code-block:: bash
$ python -W once manage.py test
The above would print all warnings once when they first occur. This is useful
to know what violations exist in your code (or occasionally in third party
code). However, it only prints the last line of the stack trace. You can use
the following to raise the full exception instead:
.. code-block:: bash
$ python -W error manage.py test
__ https://docs.python.org/3.6/using/cmdline.html#cmdoption-W
__ https://docs.python.org/3.6/using/cmdline.html#envvar-PYTHONWARNINGS
MethodFilter and Filter.action replaced by Filter.method
--------------------------------------------------------
@ -164,3 +221,11 @@ Generated filter labels in 1.0 will be more descriptive, including humanized
text about the lookup being performed and if the filter is an exclusion filter.
These settings will no longer have an effect and will be removed in the 1.0 release.
DRF filter backend raises ``TemplateDoesNotExist`` exception
------------------------------------------------------------
Templates are now provided by django-filter. If you are receiving this error,
you may need to add ``'django_filters'`` to your ``INSTALLED_APPS`` setting.
Alternatively, you could provide your own templates.

View File

@ -37,6 +37,12 @@ If you want to use the django-filter backend by default, add it to the ``DEFAULT
.. code-block:: python
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
@ -58,8 +64,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet
class ProductFilter(filters.FilterSet):
min_price = django_filters.NumberFilter(name="price", lookup_expr='gte')
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
min_price = filters.NumberFilter(name="price", lookup_expr='gte')
max_price = filters.NumberFilter(name="price", lookup_expr='lte')
class Meta:
model = Product
@ -170,3 +176,14 @@ If you are using DRF's browsable API or admin API you may also want to install `
With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so:
.. image:: ../assets/form.png
Additional ``FilterSet`` Features
---------------------------------
The following features are specific to the rest framework FilterSet:
- ``BooleanFilter``'s use the API-friendly ``BooleanWidget``, which accepts lowercase ``true``/``false``.
- Filter generation uses ``IsoDateTimeFilter`` for datetime model fields.
- Raised ``ValidationError``'s are reraised as their DRF equivalent. This behavior is useful when setting FilterSet
strictness to ``STRICTNESS.RAISE_VALIDATION_ERROR``.

View File

@ -143,7 +143,7 @@ the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
)
Solution 3: Combining fields w/ ``MultiValueField``
""""""""""""""""""""""""""""""""""""""""""""""""""
"""""""""""""""""""""""""""""""""""""""""""""""""""
An alternative approach is to use Django's ``MultiValueField`` to manually add
in a ``BooleanField`` to handle null values. Proof of concept:
@ -156,8 +156,6 @@ Filtering by an empty string
It's not currently possible to filter by an empty string, since empty values are
interpreted as a skipped filter.
.. code-block:: http
GET http://localhost/api/my-model?myfield=
Solution 1: Magic values
@ -166,8 +164,6 @@ Solution 1: Magic values
You can override the ``filter()`` method of a filter class to specifically check
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
.. code-block:: http
GET http://localhost/api/my-model?myfield=EMPTY
.. code-block:: python
@ -189,8 +185,6 @@ Solution 2: Empty string filter
It would also be possible to create an empty value filter that exhibits the same
behavior as an ``isnull`` filter.
.. code-block:: http
GET http://localhost/api/my-model?myfield__isempty=false
.. code-block:: python
@ -213,3 +207,40 @@ behavior as an ``isnull`` filter.
class Meta:
model = MyModel
Using ``initial`` values as defaults
------------------------------------
In pre-1.0 versions of django-filter, a filter field's ``initial`` value was used as a
default when no value was submitted. This behavior was not officially supported and has
since been removed.
.. warning:: It is recommended that you do **NOT** implement the below as it adversely
affects usability. Django forms don't provide this behavior for a reason.
- Using initial values as defaults is inconsistent with the behavior of Django forms.
- Default values prevent users from filtering by empty values.
- Default values prevent users from skipping that filter.
If defaults are necessary though, the following should mimic the pre-1.0 behavior:
.. code-block:: python
class BaseFilterSet(FilterSet):
def __init__(self, data=None, *args, **kwargs):
# if filterset is bound, use initial values as defaults
if data is not None:
# get a mutable copy of the QueryDict
data = data.copy()
for name, f in self.base_filters.items():
initial = f.extra.get('initial')
# filter param is either missing or empty, use initial as default
if not data.get(name) and initial:
data[name] = initial
super(BaseFilterSet, self).__init__(data, *args, **kwargs)

View File

@ -6,6 +6,11 @@ Django-filter provides a simple way to filter down a queryset based on
parameters a user provides. Say we have a ``Product`` model and we want to let
our users filter which products they see on a list page.
.. note::
If you're using django-filter with Django Rest Framework, it's
recommended that you read the integration docs after this guide.
The model
---------
@ -24,7 +29,7 @@ The filter
----------
We have a number of fields and we want to let our users filter based on the
price or the release_date. We create a ``FilterSet`` for this::
name, the price or the release_date. We create a ``FilterSet`` for this::
import django_filters
@ -169,6 +174,11 @@ a request object is passed, then you may access the request during filtering.
This allows you to filter by properties on the request, such as the currently
logged-in user or the ``Accepts-Languages`` header.
.. note::
It is not guaranteed that a `request` will be provied to the `FilterSet`
instance. Any code depending on a request should handle the `None` case.
Filtering the primary ``.qs``
"""""""""""""""""""""""""""""
@ -189,8 +199,10 @@ those that are published and those that are owned by the logged-in user
@property
def qs(self):
parent = super(ArticleFilter, self).qs
author = getattr(self.request, 'user', None)
return parent.filter(is_published=True) \
| parent.filter(author=request.user)
| parent.filter(author=author)
Filtering the related queryset for ``ModelChoiceFilter``
@ -204,6 +216,9 @@ request-based filtering without resorting to overriding ``FilterSet.__init__``.
.. code-block:: python
def departments(request):
if request is None:
return Department.objects.none()
company = request.user.company
return company.department_set.all()
@ -297,6 +312,9 @@ You must provide either a ``model`` or ``filterset_class`` argument, similar to
url(r'^list/$', FilterView.as_view(model=Product)),
]
If you provide a ``model`` optionally you can set ``filter_fields`` to specify a list or a tuple of
the fields that you want to include for the automatic construction of the filterset class.
You must provide a template at ``<app>/<model>_filter.html`` which gets the
context parameter ``filter``. Additionally, the context will contain
``object_list`` which holds the filtered queryset.

View File

@ -356,9 +356,16 @@ passed, it will be invoked with ``Filterset.request`` as its only argument.
This allows you to easily filter by properties on the request object without
having to override the ``FilterSet.__init__``.
.. note::
You should expect that the `request` object may be `None`.
.. code-block:: python
def departments(request):
if request is None:
return Department.objects.none()
company = request.user.company
return company.department_set.all()
@ -505,6 +512,14 @@ Example of using the ``DateField`` field::
# Max-Only: Comments added before 2016-02-01
f = F({'date_1': '2016-02-01'})
.. note::
When filtering ranges that occurs on DST transition dates ``DateFromToRangeFilter`` will use the first valid hour of the day for start datetime and the last valid hour of the day for end datetime.
This is OK for most applications, but if you want to customize this behavior you must extend ``DateFromToRangeFilter`` and make a custom field for it.
.. warning::
If you're using Django prior to 1.9 you may hit ``AmbiguousTimeError`` or ``NonExistentTimeError`` when start/end date matches DST start/end respectively.
This occurs because versions before 1.9 don't allow to change the DST behavior for making a datetime aware.
Example of using the ``DateTimeField`` field::
class Article(models.Model):
@ -621,7 +636,7 @@ filter is then used to validate the individual values.
Example::
class NumberInFilter(BaseInFilter, NumericFilter):
class NumberInFilter(BaseInFilter, NumberFilter):
pass
class F(FilterSet):
@ -648,7 +663,7 @@ comma-separated values.
Example::
class NumberRangeFilter(BaseInFilter, NumericFilter):
class NumberRangeFilter(BaseInFilter, NumberFilter):
pass
class F(FilterSet):
@ -742,7 +757,8 @@ want to disable descending sort options.
This filter is also CSV-based, and accepts multiple ordering params. The
default select widget does not enable the use of this, but it is useful
for APIs.
for APIs. ``SelectMultiple`` widgets are not compatible, given that they
are not able to retain selection order.
Adding Custom filter choices
""""""""""""""""""""""""""""
@ -757,14 +773,16 @@ If you wish to sort by non-model fields, you'll need to add custom handling to a
def __init__(self, *args, **kwargs):
super(CustomOrderingFilter, self).__init__(*args, **kwargs)
self.choices += [
self.extra['choices'] += [
('relevance', 'Relevance'),
('-relevance', 'Relevance (descending)'),
]
def filter(self, qs, value):
if value in ['relevance', '-relevance']:
# OrderingFilter is CSV-based, so `value` is a list
if any(v in ['relevance', '-relevance'] for v in value):
# sort queryset by relevance
return ...
return super(CustomOrderingFilter, self).filter(qs, value)

View File

@ -152,17 +152,33 @@ This is a map of model fields to filter classes with options::
.. _strict:
``strict``
~~~~~~~~~~
Handling validation errors with ``strict``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``strict`` option determines the filterset's behavior when filters fail to validate. Example use:
.. code-block:: python
from django_filters import FilterSet, STRICTNESS
class ProductFilter(FilterSet):
class Meta:
model = Product
fields = ['name', 'release_date']
strict = STRICTNESS.RETURN_NO_RESULTS
Currently, there are three different behaviors:
- ``STRICTNESS.RETURN_NO_RESULTS`` (default) This returns an empty queryset. The
filterset form can then be rendered to display the input errors.
- ``STRICTNESS.IGNORE`` Instead of returning an empty queryset, invalid filters
effectively become a noop. Valid filters are applied to the queryset however.
- ``STRICTNESS.RAISE_VALIDATION_ERROR`` This raises a ``ValidationError`` for
all invalid filters. This behavior is generally useful with APIs.
If the ``strict`` option is not provided, then the filterset will default to the
value of the ``FILTERS_STRICTNESS`` setting.
The ``strict`` option controls whether results are returned when an invalid
value is specified by the user for any filter field. By default, ``strict`` is
set to ``STRICTNESS.RETURN_NO_RESULTS`` meaning that an empty queryset is
returned if any field contains an invalid value. You can loosen this behavior
by setting ``strict`` to ``STRICTNESS.IGNORE`` which will effectively ignore a
filter field if its value is invalid. A third option of
``STRICTNESS.RAISE_VALIDATION_ERROR`` will cause a ``ValidationError`` to be
raised if any field contains an invalid value.
Overriding ``FilterSet`` methods
--------------------------------

View File

@ -94,4 +94,13 @@ FILTERS_STRICTNESS
Default: ``STRICTNESS.RETURN_NO_RESULTS``
Set the global default for FilterSet :ref:`strictness <strict>`.
Set the global default for FilterSet :ref:`strictness <strict>`. If ``strict`` is not
provided to the filterset, it will default to this setting. You can change the setting
like so:
.. code-block:: python
# settings.py
from django_filters import STRICTNESS
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS

View File

@ -61,3 +61,28 @@ a ``RangeField``:
.. code-block:: python
date_range = DateFromToRangeFilter(widget=RangeWidget(attrs={'placeholder': 'YYYY/MM/DD'}))
``SuffixedMultiWidget``
~~~~~~~~~~~~~~~~~~~~~~~
Extends Django's builtin ``MultiWidget`` to append custom suffixes instead of
indices. For example, take a range widget that accepts minimum and maximum
bounds. By default, the resulting query params would look like the following:
.. code-block:: http
GET /products?price_0=10&price_1=25 HTTP/1.1
By using ``SuffixedMultiWidget`` instead, you can provide human-friendly suffixes.
.. code-block:: python
class RangeWidget(SuffixedMultiWidget):
suffixes = ['min', 'max']
The query names are now a little more ergonomic.
.. code-block:: http
GET /products?price_min=10&price_max=25 HTTP/1.1

View File

@ -1,4 +1,6 @@
markdown==2.6.4
coreapi
django-crispy-forms
coverage
mock

View File

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

View File

@ -6,7 +6,7 @@ f = open('README.rst')
readme = f.read()
f.close()
version = '1.0.1'
version = '1.1.0'
if sys.argv[-1] == 'publish':
if os.system("pip freeze | grep wheel"):
@ -32,13 +32,9 @@ setup(
author_email='alex.gaynor@gmail.com',
maintainer='Carlton Gibson',
maintainer_email='carlton.gibson@noumenal.es',
url='http://github.com/carltongibson/django-filter/tree/master',
packages=find_packages(exclude=['tests']),
package_data={
'django_filters': [
'locale/*/LC_MESSAGES/*',
],
},
url='https://github.com/carltongibson/django-filter/tree/master',
packages=find_packages(exclude=['tests*']),
include_package_data=True,
license='BSD',
classifiers=[
'Development Status :: 5 - Production/Stable',
@ -46,14 +42,19 @@ setup(
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Framework :: Django',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
],
include_package_data=True,
zip_safe=False,
)

View File

@ -1,12 +1,10 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from django import forms
from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
REGULAR = 0
MANAGER = 1
ADMIN = 2
@ -131,6 +129,8 @@ class NetworkSetting(models.Model):
ip = models.GenericIPAddressField()
mask = SubnetMaskField()
cidr = models.CharField(max_length=18, blank=True, verbose_name="CIDR")
@python_2_unicode_compatible
class Company(models.Model):

View File

@ -1,28 +1,24 @@
from __future__ import unicode_literals
import datetime
import warnings
from decimal import Decimal
from unittest import skipIf
from django.conf.urls import url
from django.db.models import BooleanField
from django.test import TestCase
from django.test.utils import override_settings
from django.utils.dateparse import parse_date
try:
from django.urls import reverse
except ImportError:
# Django < 1.10 compatibility
from django.core.urlresolvers import reverse
from rest_framework import generics, serializers, status
from rest_framework import generics, serializers
from rest_framework.test import APIRequestFactory
from django_filters import compat, filters
from django_filters.rest_framework import DjangoFilterBackend, FilterSet
from django_filters.rest_framework import backends
from django_filters.rest_framework import (
DjangoFilterBackend,
FilterSet,
backends
)
from .models import BaseFilterableItem, BasicModel, FilterableItem, DjangoFilterOrderingModel
from .models import FilterableItem
factory = APIRequestFactory()
@ -59,69 +55,6 @@ class FilterClassRootView(generics.ListCreateAPIView):
filter_backends = (DjangoFilterBackend,)
# These classes are used to test a misconfigured filter class.
class MisconfiguredFilter(FilterSet):
text = filters.CharFilter(lookup_expr='icontains')
class Meta:
model = BasicModel
fields = ['text']
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = MisconfiguredFilter
filter_backends = (DjangoFilterBackend,)
class FilterClassDetailView(generics.RetrieveAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
# These classes are used to test base model filter support
class BaseFilterableItemFilter(FilterSet):
text = filters.CharFilter()
class Meta:
model = BaseFilterableItem
fields = '__all__'
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = BaseFilterableItemFilter
filter_backends = (DjangoFilterBackend,)
# Regression test for #814
class FilterFieldsQuerysetView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date']
filter_backends = (DjangoFilterBackend,)
class GetQuerysetView(generics.ListCreateAPIView):
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (DjangoFilterBackend,)
def get_queryset(self):
return FilterableItem.objects.all()
urlpatterns = [
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
url(r'^get-queryset/$', GetQuerysetView.as_view(), name='get-queryset-view'),
]
@skipIf(compat.coreapi is None, 'coreapi must be installed')
class GetSchemaFieldsTests(TestCase):
def test_fields_with_filter_fields_list(self):
@ -131,6 +64,27 @@ class GetSchemaFieldsTests(TestCase):
self.assertEqual(fields, ['decimal', 'date'])
def test_filter_fields_list_with_bad_get_queryset(self):
"""
See:
* https://github.com/carltongibson/django-filter/issues/551
"""
class BadGetQuerySetView(FilterFieldsRootView):
def get_queryset(self):
raise AttributeError("I don't have that")
backend = DjangoFilterBackend()
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
fields = backend.get_schema_fields(BadGetQuerySetView())
self.assertEqual(fields, [], "get_schema_fields should handle AttributeError")
warning = "{} is not compatible with schema generation".format(BadGetQuerySetView)
self.assertEqual(len(w), 1)
self.assertEqual(str(w[0].message), warning)
def test_fields_with_filter_fields_dict(self):
class DictFilterFieldsRootView(FilterFieldsRootView):
filter_fields = {
@ -146,181 +100,57 @@ class GetSchemaFieldsTests(TestCase):
def test_fields_with_filter_class(self):
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(FilterClassRootView())
schemas = [f.schema for f in fields]
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date'])
self.assertIsInstance(schemas[0], compat.coreschema.String)
self.assertIsInstance(schemas[1], compat.coreschema.Number)
self.assertIsInstance(schemas[2], compat.coreschema.String)
def test_field_required(self):
class RequiredFieldsFilter(SeveralFieldsFilter):
required_text = filters.CharFilter(required=True)
class Meta(SeveralFieldsFilter.Meta):
fields = SeveralFieldsFilter.Meta.fields + ['required_text']
class FilterClassWithRequiredFieldsView(FilterClassRootView):
filter_class = RequiredFieldsFilter
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(FilterClassWithRequiredFieldsView())
required = [f.required for f in fields]
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date', 'required_text'])
self.assertFalse(required[0])
self.assertFalse(required[1])
self.assertFalse(required[2])
self.assertTrue(required[3])
def tests_field_with_request_callable(self):
def qs(request):
# users expect a valid request object to be provided which cannot
# be guaranteed during schema generation.
self.fail("callable queryset should not be invoked during schema generation")
class F(SeveralFieldsFilter):
f = filters.ModelChoiceFilter(queryset=qs)
class View(FilterClassRootView):
filter_class = F
view = View()
view.request = factory.get('/')
backend = DjangoFilterBackend()
fields = backend.get_schema_fields(view)
fields = [f.name for f in fields]
self.assertEqual(fields, ['text', 'decimal', 'date', 'f'])
class CommonFilteringTestCase(TestCase):
def _serialize_object(self, obj):
return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
def setUp(self):
"""
Create 10 FilterableItem instances.
"""
base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
for i in range(10):
text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
decimal = base_data[1] + i
date = base_data[2] - datetime.timedelta(days=i * 2)
FilterableItem(text=text, decimal=decimal, date=date).save()
self.objects = FilterableItem.objects
self.data = [
self._serialize_object(obj)
for obj in self.objects.all()
]
class IntegrationTestFiltering(CommonFilteringTestCase):
"""
Integration tests for filtered list views.
"""
def test_get_filtered_fields_root_view(self):
"""
GET requests to paginated ListCreateAPIView should return paginated results.
"""
view = FilterFieldsRootView.as_view()
# Basic test with no filter.
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
# Tests that the decimal filter works.
search_decimal = Decimal('2.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter works.
search_date = datetime.date(2012, 9, 22)
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
self.assertEqual(response.data, expected_data)
def test_filter_with_queryset(self):
"""
Regression test for #814.
"""
view = FilterFieldsQuerysetView.as_view()
# Tests that the decimal filter works.
search_decimal = Decimal('2.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
def test_filter_with_get_queryset_only(self):
"""
Regression test for #834.
"""
view = GetQuerysetView.as_view()
request = factory.get('/get-queryset/')
view(request).render()
# Used to raise "issubclass() arg 2 must be a class or tuple of classes"
# here when neither `model' nor `queryset' was specified.
def test_get_filtered_class_root_view(self):
"""
GET requests to filtered ListCreateAPIView that have a filter_class set
should return filtered results.
"""
view = FilterClassRootView.as_view()
# Basic test with no filter.
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
# Tests that the decimal filter set with 'lt' in the filter class works.
search_decimal = Decimal('4.25')
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter set with 'gt' in the filter class works.
search_date = datetime.date(2012, 10, 2)
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
self.assertEqual(response.data, expected_data)
# Tests that the text filter set with 'icontains' in the filter class works.
search_text = 'ff'
request = factory.get('/', {'text': '%s' % search_text})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if search_text in f['text'].lower()]
self.assertEqual(response.data, expected_data)
# Tests that multiple filters works.
search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2)
request = factory.get('/', {
'decimal': '%s' % (search_decimal,),
'date': '%s' % (search_date,)
})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
def test_incorrectly_configured_filter(self):
"""
An error should be displayed when the filter class is misconfigured.
"""
view = IncorrectlyConfiguredRootView.as_view()
request = factory.get('/')
self.assertRaises(AssertionError, view, request)
def test_base_model_filter(self):
"""
The `get_filter_class` model checks should allow base model filters.
"""
view = BaseFilterableItemFilterRootView.as_view()
request = factory.get('/?text=aaa')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
def test_unknown_filter(self):
"""
GET requests with filters that aren't configured should return 200.
"""
view = FilterFieldsRootView.as_view()
search_integer = 10
request = factory.get('/', {'integer': '%s' % search_integer})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_html_rendering(self):
"""
Make sure response renders w/ backend
"""
view = FilterFieldsRootView.as_view()
request = factory.get('/')
request.META['HTTP_ACCEPT'] = 'text/html'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
class TemplateTests(TestCase):
def test_backend_output(self):
"""
Ensure backend renders default if template path does not exist
@ -367,99 +197,33 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
reload(backends)
def test_multiple_engines(self):
# See: https://github.com/carltongibson/django-filter/issues/578
DTL = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True}
ALT = {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, 'NAME': 'alt'}
@override_settings(ROOT_URLCONF='tests.rest_framework.test_backends')
class IntegrationTestDetailFiltering(CommonFilteringTestCase):
"""
Integration tests for filtered detail views.
"""
def _get_url(self, item):
return reverse('detail-view', kwargs=dict(pk=item.pk))
def test_get_filtered_detail_view(self):
"""
GET requests to filtered RetrieveAPIView that have a filter_class set
should return filtered results.
"""
item = self.objects.all()[0]
data = self._serialize_object(item)
# Basic test with no filter.
response = self.client.get(self._get_url(item))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, data)
# Tests that the decimal filter set that should fail.
search_decimal = Decimal('4.25')
high_item = self.objects.filter(decimal__gt=search_decimal)[0]
response = self.client.get(
'{url}'.format(url=self._get_url(high_item)),
{'decimal': '{param}'.format(param=search_decimal)})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
# Tests that the decimal filter set that should succeed.
search_decimal = Decimal('4.25')
low_item = self.objects.filter(decimal__lt=search_decimal)[0]
low_item_data = self._serialize_object(low_item)
response = self.client.get(
'{url}'.format(url=self._get_url(low_item)),
{'decimal': '{param}'.format(param=search_decimal)})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, low_item_data)
# Tests that multiple filters works.
search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2)
valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
valid_item_data = self._serialize_object(valid_item)
response = self.client.get(
'{url}'.format(url=self._get_url(valid_item)), {
'decimal': '{decimal}'.format(decimal=search_decimal),
'date': '{date}'.format(date=search_date)
})
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, valid_item_data)
# multiple DTL backends
with override_settings(TEMPLATES=[DTL, ALT]):
self.test_backend_output()
class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
class Meta:
model = DjangoFilterOrderingModel
fields = '__all__'
class DefaultFilterSetTests(TestCase):
def test_default_meta_inheritance(self):
# https://github.com/carltongibson/django-filter/issues/663
class F(FilterSet):
class Meta:
filter_overrides = {BooleanField: {}}
class DjangoFilterOrderingTests(TestCase):
def setUp(self):
data = [{
'date': datetime.date(2012, 10, 8),
'text': 'abc'
}, {
'date': datetime.date(2013, 10, 8),
'text': 'bcd'
}, {
'date': datetime.date(2014, 10, 8),
'text': 'cde'
}]
class Backend(DjangoFilterBackend):
default_filter_set = F
for d in data:
DjangoFilterOrderingModel.objects.create(**d)
view = FilterFieldsRootView()
backend = Backend()
def test_default_ordering(self):
class DjangoFilterOrderingView(generics.ListAPIView):
serializer_class = DjangoFilterOrderingSerializer
queryset = DjangoFilterOrderingModel.objects.all()
filter_backends = (DjangoFilterBackend,)
filter_fields = ['text']
ordering = ('-date',)
filter_class = backend.get_filter_class(view, view.get_queryset())
filter_overrides = filter_class._meta.filter_overrides
view = DjangoFilterOrderingView.as_view()
request = factory.get('/')
response = view(request)
self.assertEqual(
response.data,
[
{'id': 3, 'date': '2014-10-08', 'text': 'cde'},
{'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
{'id': 1, 'date': '2012-10-08', 'text': 'abc'}
]
)
# derived filter_class.Meta should inherit from default_filter_set.Meta
self.assertIn(BooleanField, filter_overrides)
self.assertDictEqual(filter_overrides[BooleanField], {})

View File

@ -12,4 +12,4 @@ class BooleanFilterTests(TestCase):
# from `rest_framework.filters`.
f = filters.BooleanFilter()
self.assertEqual(f.widget, BooleanWidget)
self.assertEqual(f.extra['widget'], BooleanWidget)

View File

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

View File

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

View File

@ -1,3 +1,8 @@
# ensure package/conf is importable
from django_filters import STRICTNESS
from django_filters.conf import DEFAULTS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
@ -34,5 +39,7 @@ STATIC_URL = '/static/'
# help verify that DEFAULTS is importable from conf.
def FILTERS_VERBOSE_LOOKUPS():
from django_filters.conf import DEFAULTS
return DEFAULTS['VERBOSE_LOOKUPS']
FILTERS_STRICTNESS = STRICTNESS.RETURN_NO_RESULTS

1339
tests/tags Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,8 @@
from django.test import TestCase, override_settings
from django_filters.conf import settings
from django_filters import FilterSet, STRICTNESS
from django_filters import STRICTNESS, FilterSet
from django_filters.conf import is_callable, settings
from tests.models import User
@ -119,3 +118,24 @@ class OverrideSettingsTests(TestCase):
self.assertFalse(hasattr(settings, 'FILTERS_FOOBAR'))
self.assertFalse(hasattr(settings, 'FOOBAR'))
class IsCallableTests(TestCase):
def test_behavior(self):
def func():
pass
class Class(object):
def __call__(self):
pass
def method(self):
pass
c = Class()
self.assertTrue(is_callable(func))
self.assertFalse(is_callable(Class))
self.assertTrue(is_callable(c))
self.assertTrue(is_callable(c.method))

View File

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

View File

@ -1,18 +1,25 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from datetime import datetime, time, timedelta, tzinfo
import decimal
from datetime import datetime, time, timedelta, tzinfo
import pytz
from django import forms
from django.test import TestCase, override_settings
from django.utils.timezone import make_aware, get_default_timezone
from django.utils import timezone
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
from django_filters.fields import (
Lookup, LookupTypeField, BaseCSVField, BaseRangeField, RangeField,
DateRangeField, DateTimeRangeField, TimeRangeField, IsoDateTimeField
BaseCSVField,
BaseRangeField,
DateRangeField,
DateTimeRangeField,
IsoDateTimeField,
Lookup,
LookupTypeField,
RangeField,
TimeRangeField
)
from django_filters.widgets import BaseCSVWidget, CSVWidget, RangeWidget
def to_d(float_value):
@ -41,11 +48,12 @@ class RangeFieldTests(TestCase):
def test_clean(self):
w = RangeWidget()
f = RangeField(widget=w)
f = RangeField(widget=w, required=False)
self.assertEqual(
f.clean(['12.34', '55']),
slice(to_d(12.34), to_d(55)))
self.assertIsNone(f.clean([]))
class DateRangeFieldTests(TestCase):
@ -57,11 +65,12 @@ class DateRangeFieldTests(TestCase):
@override_settings(USE_TZ=False)
def test_clean(self):
w = RangeWidget()
f = DateRangeField(widget=w)
f = DateRangeField(widget=w, required=False)
self.assertEqual(
f.clean(['2015-01-01', '2015-01-10']),
slice(datetime(2015, 1, 1, 0, 0, 0),
datetime(2015, 1, 10, 23, 59, 59, 999999)))
self.assertIsNone(f.clean([]))
class DateTimeRangeFieldTests(TestCase):
@ -104,10 +113,13 @@ class LookupTypeFieldTests(TestCase):
def test_clean(self):
inner = forms.DecimalField()
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')])
f = LookupTypeField(inner, [('gt', 'gt'), ('lt', 'lt')], required=False)
self.assertEqual(
f.clean(['12.34', 'lt']),
Lookup(to_d(12.34), 'lt'))
self.assertEqual(
f.clean([]),
Lookup(value=None, lookup_type='exact'))
def test_render_used_html5(self):
inner = forms.DecimalField()
@ -129,48 +141,68 @@ class LookupTypeFieldTests(TestCase):
class IsoDateTimeFieldTests(TestCase):
reference_str = "2015-07-19T13:34:51.759"
reference_dt = datetime(2015, 7, 19, 13, 34, 51, 759000)
field = IsoDateTimeField()
def parse_input(self, value):
return self.field.strptime(value, IsoDateTimeField.ISO_8601)
def test_datetime_string_is_parsed(self):
f = IsoDateTimeField()
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
d = self.parse_input(self.reference_str)
self.assertTrue(isinstance(d, datetime))
def test_datetime_string_with_timezone_is_parsed(self):
f = IsoDateTimeField()
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
d = self.parse_input(self.reference_str + "+01:00")
self.assertTrue(isinstance(d, datetime))
def test_datetime_zulu(self):
f = IsoDateTimeField()
d = f.strptime(self.reference_str + "Z", IsoDateTimeField.ISO_8601)
d = self.parse_input(self.reference_str + "Z")
self.assertTrue(isinstance(d, datetime))
@override_settings(TIME_ZONE='UTC')
def test_datetime_timezone_awareness(self):
# parsed datetimes should obey USE_TZ
f = IsoDateTimeField()
r = make_aware(self.reference_dt, get_default_timezone())
utc, tokyo = pytz.timezone('UTC'), pytz.timezone('Asia/Tokyo')
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
self.assertTrue(isinstance(d.tzinfo, tzinfo))
self.assertEqual(d, r + r.utcoffset() - d.utcoffset())
# by default, use the server timezone
reference = utc.localize(self.reference_dt)
parsed = self.parse_input(self.reference_str)
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed, reference)
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
self.assertTrue(isinstance(d.tzinfo, tzinfo))
self.assertEqual(d, r)
# if set, use the active timezone
reference = tokyo.localize(self.reference_dt)
with timezone.override(tokyo):
parsed = self.parse_input(self.reference_str)
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed.tzinfo.zone, tokyo.zone)
self.assertEqual(parsed, reference)
# if provided, utc offset should have precedence
reference = utc.localize(self.reference_dt - timedelta(hours=1))
parsed = self.parse_input(self.reference_str + "+01:00")
self.assertIsInstance(parsed.tzinfo, tzinfo)
self.assertEqual(parsed, reference)
@override_settings(USE_TZ=False)
def test_datetime_timezone_naivety(self):
# parsed datetimes should obey USE_TZ
reference = self.reference_dt.replace()
parsed = self.parse_input(self.reference_str + "+01:00")
self.assertIsNone(parsed.tzinfo)
self.assertEqual(parsed, reference - timedelta(hours=1))
parsed = self.parse_input(self.reference_str)
self.assertIsNone(parsed.tzinfo)
self.assertEqual(parsed, reference)
def test_datetime_non_iso_format(self):
f = IsoDateTimeField()
r = self.reference_dt.replace()
parsed = f.strptime('19-07-2015T51:34:13.759', '%d-%m-%YT%S:%M:%H.%f')
self.assertTrue(isinstance(parsed, datetime))
self.assertEqual(parsed, self.reference_dt)
d = f.strptime(self.reference_str + "+01:00", IsoDateTimeField.ISO_8601)
self.assertTrue(d.tzinfo is None)
self.assertEqual(d, r - timedelta(hours=1))
d = f.strptime(self.reference_str + "", IsoDateTimeField.ISO_8601)
self.assertTrue(d.tzinfo is None)
self.assertEqual(d, r)
def test_datetime_wrong_format(self):
with self.assertRaises(ValueError):
self.parse_input('19-07-2015T51:34:13.759')
class BaseCSVFieldTests(TestCase):
@ -202,9 +234,10 @@ class BaseCSVFieldTests(TestCase):
self.assertIn("'BaseCSVField.widget' must be a widget class", msg)
self.assertIn("RangeWidget", msg)
widget = CSVWidget()
widget = CSVWidget(attrs={'class': 'class'})
field = BaseCSVField(widget=widget)
self.assertIs(field.widget, widget)
self.assertIsInstance(field.widget, CSVWidget)
self.assertEqual(field.widget.attrs, {'class': 'class'})
field = BaseCSVField(widget=CSVWidget)
self.assertIsInstance(field.widget, CSVWidget)

View File

@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
import datetime
import mock
@ -8,42 +7,45 @@ import unittest
import django
from django import forms
from django.test import TestCase, override_settings
from django.utils import six
from django.utils import six, timezone
from django.utils.timezone import now
from django.utils import timezone
from django_filters.filterset import FilterSet
from django_filters.filters import AllValuesFilter
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filters import CharFilter
from django_filters.filters import ChoiceFilter
from django_filters.filters import DateRangeFilter
from django_filters.filters import DateFromToRangeFilter
from django_filters.filters import DateTimeFromToRangeFilter
from django_filters.filters import DurationFilter
from django_filters.filters import MultipleChoiceFilter
from django_filters.filters import ModelChoiceFilter
from django_filters.filters import TypedMultipleChoiceFilter
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import NumberFilter
from django_filters.filters import OrderingFilter
from django_filters.filters import RangeFilter
from django_filters.filters import TimeRangeFilter
from django_filters.exceptions import FieldLookupError
from django_filters.filters import (
AllValuesFilter,
AllValuesMultipleFilter,
CharFilter,
ChoiceFilter,
DateFromToRangeFilter,
DateRangeFilter,
DateTimeFromToRangeFilter,
DurationFilter,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
MultipleChoiceFilter,
NumberFilter,
OrderingFilter,
RangeFilter,
TimeRangeFilter,
TypedMultipleChoiceFilter
)
from django_filters.filterset import FilterSet
from .models import User
from .models import Comment
from .models import Book
from .models import Article
from .models import Company
from .models import Location
from .models import Account
from .models import BankAccount
from .models import Profile
from .models import Node
from .models import DirectedNode
from .models import STATUS_CHOICES
from .models import SpacewalkRecord
from .models import (
STATUS_CHOICES,
Account,
Article,
BankAccount,
Book,
Comment,
Company,
DirectedNode,
Location,
Node,
Profile,
SpacewalkRecord,
User
)
class CharFilterTests(TestCase):
@ -252,6 +254,44 @@ class MultipleChoiceFilterTests(TestCase):
self.assertQuerysetEqual(
f.qs, ['aaron', 'alex', 'carl', 'jacob'], lambda o: o.username)
def test_filtering_on_null_choice(self):
User.objects.create(username='alex', status=1)
User.objects.create(username='jacob', status=2)
User.objects.create(username='aaron', status=2)
User.objects.create(username='carl', status=0)
Article.objects.create(author_id=1, published=now())
Article.objects.create(author_id=2, published=now())
Article.objects.create(author_id=3, published=now())
Article.objects.create(author_id=4, published=now())
Article.objects.create(author_id=None, published=now())
choices = [(u.pk, str(u)) for u in User.objects.order_by('id')]
class F(FilterSet):
author = MultipleChoiceFilter(
choices=choices,
null_value='null',
null_label='NULL',
)
class Meta:
model = Article
fields = ['author']
# sanity check to make sure the filter is setup correctly
f = F({'author': ['1']})
self.assertQuerysetEqual(f.qs, ['alex'], lambda o: str(o.author), False)
f = F({'author': ['null']})
self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False)
f = F({'author': ['1', 'null']})
self.assertQuerysetEqual(
f.qs, ['alex', None],
lambda o: o.author and str(o.author),
False)
class TypedMultipleChoiceFilterTests(TestCase):
@ -485,6 +525,21 @@ class ModelChoiceFilterTests(TestCase):
f = F({'author': jacob.pk}, queryset=qs)
self.assertQuerysetEqual(f.qs, [1, 3], lambda o: o.pk, False)
@override_settings(FILTERS_NULL_CHOICE_LABEL='No Author')
def test_filtering_null(self):
Article.objects.create(published=now())
alex = User.objects.create(username='alex')
Article.objects.create(author=alex, published=now())
class F(FilterSet):
class Meta:
model = Article
fields = ['author', 'name']
qs = Article.objects.all()
f = F({'author': 'null'}, queryset=qs)
self.assertQuerysetEqual(f.qs, [None], lambda o: o.author, False)
def test_callable_queryset(self):
# Sanity check for callable queryset arguments.
# Ensure that nothing is improperly cached
@ -528,8 +583,8 @@ class ModelMultipleChoiceFilterTests(TestCase):
average_rating=3.0)
Book.objects.create(title="Stranger in a Strage Land", price='1.00',
average_rating=3.0)
alex.favorite_books = [b1, b2]
aaron.favorite_books = [b1, b3]
alex.favorite_books.add(b1, b2)
aaron.favorite_books.add(b1, b3)
self.alex = alex
@ -552,6 +607,18 @@ class ModelMultipleChoiceFilterTests(TestCase):
f = F({'favorite_books': ['4']}, queryset=qs)
self.assertQuerysetEqual(f.qs, [], lambda o: o.username)
@override_settings(FILTERS_NULL_CHOICE_LABEL='No Favorites')
def test_filtering_null(self):
class F(FilterSet):
class Meta:
model = User
fields = ['favorite_books']
qs = User.objects.all()
f = F({'favorite_books': ['null']}, queryset=qs)
self.assertQuerysetEqual(f.qs, ['jacob'], lambda o: o.username)
def test_filtering_dictionary(self):
class F(FilterSet):
class Meta:
@ -584,7 +651,7 @@ class ModelMultipleChoiceFilterTests(TestCase):
'queryset': Book.objects.filter(id__in=[1, 2])
})
self.filters['favorite_books'].required = True
self.filters['favorite_books'].extra['required'] = True
qs = User.objects.all().order_by('username')
@ -874,6 +941,92 @@ class DateFromToRangeFilterTests(TestCase):
'published_1': '2016-01-03'})
self.assertEqual(len(results.qs), 3)
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_filtering_dst_start_midnight(self):
tz = timezone.get_default_timezone()
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 14, 23, 59)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 0, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 15, 1, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 16, 0, 0)))
class F(FilterSet):
published = DateFromToRangeFilter()
class Meta:
model = Article
fields = ['published']
results = F(data={
'published_0': '2017-10-15',
'published_1': '2017-10-15'})
self.assertEqual(len(results.qs), 2)
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_filtering_dst_ends_midnight(self):
tz = timezone.get_default_timezone()
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 19, 0, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 23, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 18, 0, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 2, 17, 15, 0)))
class F(FilterSet):
published = DateFromToRangeFilter()
class Meta:
model = Article
fields = ['published']
results = F(data={
'published_0': '2017-02-18',
'published_1': '2017-02-18'})
self.assertEqual(len(results.qs), 2)
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='Europe/Paris')
def test_filtering_dst_start(self):
tz = timezone.get_default_timezone()
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 25, 23, 59)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 0, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 2, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 26, 3, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 3, 27, 0, 0)))
class F(FilterSet):
published = DateFromToRangeFilter()
class Meta:
model = Article
fields = ['published']
results = F(data={
'published_0': '2017-3-26',
'published_1': '2017-3-26'})
self.assertEqual(len(results.qs), 3)
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='Europe/Paris')
def test_filtering_dst_end(self):
tz = timezone.get_default_timezone()
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 28, 23, 59)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 0, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 2, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 29, 3, 0)))
Article.objects.create(published=tz.localize(datetime.datetime(2017, 10, 30, 0, 0)))
class F(FilterSet):
published = DateFromToRangeFilter()
class Meta:
model = Article
fields = ['published']
results = F(data={
'published_0': '2017-10-29',
'published_1': '2017-10-29'})
self.assertEqual(len(results.qs), 3)
class DateTimeFromToRangeFilterTests(TestCase):
@ -1290,8 +1443,8 @@ class M2MRelationshipTests(TestCase):
average_rating=4.0)
Book.objects.create(title="Stranger in a Strage Land", price='2.00',
average_rating=3.0)
alex.favorite_books = [b1, b2]
aaron.favorite_books = [b1, b3]
alex.favorite_books.add(b1, b2)
aaron.favorite_books.add(b1, b3)
def test_m2m_relation(self):
class F(FilterSet):

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from collections import OrderedDict
from datetime import date, time, timedelta, datetime
import inspect
import mock
from collections import OrderedDict
from datetime import date, datetime, time, timedelta
from django import forms
from django.test import TestCase, override_settings
@ -14,41 +13,42 @@ from django.utils.translation import ugettext as _
from django_filters import filters, widgets
from django_filters.fields import (
Lookup,
RangeField,
BaseCSVField,
DateRangeField,
DateTimeRangeField,
TimeRangeField,
Lookup,
LookupTypeField,
BaseCSVField)
RangeField,
TimeRangeField
)
from django_filters.filters import (
Filter,
CharFilter,
BooleanFilter,
ChoiceFilter,
MultipleChoiceFilter,
TypedMultipleChoiceFilter,
DateFilter,
DateTimeFilter,
TimeFilter,
DurationFilter,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
NumberFilter,
NumericRangeFilter,
RangeFilter,
DateRangeFilter,
DateFromToRangeFilter,
DateTimeFromToRangeFilter,
TimeRangeFilter,
LOOKUP_TYPES,
AllValuesFilter,
BaseCSVFilter,
BaseInFilter,
BaseRangeFilter,
UUIDFilter,
BooleanFilter,
CharFilter,
ChoiceFilter,
DateFilter,
DateFromToRangeFilter,
DateRangeFilter,
DateTimeFilter,
DateTimeFromToRangeFilter,
DurationFilter,
Filter,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
MultipleChoiceFilter,
NumberFilter,
NumericRangeFilter,
OrderingFilter,
LOOKUP_TYPES)
RangeFilter,
TimeFilter,
TimeRangeFilter,
TypedMultipleChoiceFilter,
UUIDFilter
)
from tests.models import Book, User
@ -132,17 +132,16 @@ class FilterTests(TestCase):
f.field
mocked.assert_called_once_with(required=mock.ANY,
label=mock.ANY,
widget=mock.ANY,
someattr='someattr')
def test_field_with_required_filter(self):
def test_field_required_default(self):
# filter form fields should not be required by default
with mock.patch.object(Filter, 'field_class',
spec=['__call__']) as mocked:
f = Filter(required=True)
f = Filter()
f.field
mocked.assert_called_once_with(required=True,
label=mock.ANY,
widget=mock.ANY)
mocked.assert_called_once_with(required=False,
label=mock.ANY)
def test_filtering(self):
qs = mock.Mock(spec=['filter'])
@ -313,35 +312,35 @@ class ChoiceFilterTests(TestCase):
def test_empty_choice(self):
# default value
f = ChoiceFilter(choices=[('a', 'a')])
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('a', 'a'),
])
# set value, allow blank label
f = ChoiceFilter(choices=[('a', 'a')], empty_label='')
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', ''),
('a', 'a'),
])
# disable empty choice w/ None
f = ChoiceFilter(choices=[('a', 'a')], empty_label=None)
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('a', 'a'),
])
def test_null_choice(self):
# default is to be disabled
f = ChoiceFilter(choices=[('a', 'a')], )
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('a', 'a'),
])
# set label, allow blank label
f = ChoiceFilter(choices=[('a', 'a')], null_label='')
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('null', ''),
('a', 'a'),
@ -349,7 +348,7 @@ class ChoiceFilterTests(TestCase):
# set null value
f = ChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='')
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('NULL', ''),
('a', 'a'),
@ -357,35 +356,73 @@ class ChoiceFilterTests(TestCase):
# explicitly disable
f = ChoiceFilter(choices=[('a', 'a')], null_label=None)
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('a', 'a'),
])
def test_null_multiplechoice(self):
# default is to be disabled
f = MultipleChoiceFilter(choices=[('a', 'a')], )
self.assertEqual(list(f.field.choices), [
('a', 'a'),
])
# set label, allow blank label
f = MultipleChoiceFilter(choices=[('a', 'a')], null_label='')
self.assertEqual(list(f.field.choices), [
('null', ''),
('a', 'a'),
])
# set null value
f = MultipleChoiceFilter(choices=[('a', 'a')], null_value='NULL', null_label='')
self.assertEqual(list(f.field.choices), [
('NULL', ''),
('a', 'a'),
])
# explicitly disable
f = MultipleChoiceFilter(choices=[('a', 'a')], null_label=None)
self.assertEqual(list(f.field.choices), [
('a', 'a'),
])
@override_settings(
FILTERS_EMPTY_CHOICE_LABEL='EMPTY LABEL',
FILTERS_NULL_CHOICE_LABEL='NULL LABEL',
FILTERS_NULL_CHOICE_VALUE='NULL VALUE', )
def test_settings_overrides(self):
f = ChoiceFilter(choices=[('a', 'a')], )
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', 'EMPTY LABEL'),
('NULL VALUE', 'NULL LABEL'),
('a', 'a'),
])
f = MultipleChoiceFilter(choices=[('a', 'a')], )
self.assertEqual(list(f.field.choices), [
('NULL VALUE', 'NULL LABEL'),
('a', 'a'),
])
def test_callable_choices(self):
def choices():
yield ('a', 'a')
yield ('b', 'b')
f = ChoiceFilter(choices=choices)
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('a', 'a'),
('b', 'b'),
])
def test_callable_choices_is_lazy(self):
def choices():
self.fail('choices should not be called during initialization')
ChoiceFilter(choices=choices)
class MultipleChoiceFilterTests(TestCase):
@ -700,6 +737,16 @@ class ModelChoiceFilterTests(TestCase):
with self.assertRaises(TypeError):
f.field
@override_settings(
FILTERS_EMPTY_CHOICE_LABEL='EMPTY',
FILTERS_NULL_CHOICE_VALUE='NULL', )
def test_empty_choices(self):
f = ModelChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL')
self.assertEqual(list(f.field.choices), [
('', 'EMPTY'),
('null', 'NULL'),
])
def test_default_field_with_queryset(self):
qs = mock.NonCallableMock(spec=[])
f = ModelChoiceFilter(queryset=qs)
@ -742,6 +789,15 @@ class ModelMultipleChoiceFilterTests(TestCase):
with self.assertRaises(TypeError):
f.field
@override_settings(
FILTERS_EMPTY_CHOICE_LABEL='EMPTY',
FILTERS_NULL_CHOICE_VALUE='NULL', )
def test_empty_choices(self):
f = ModelMultipleChoiceFilter(queryset=User.objects.all(), null_value='null', null_label='NULL')
self.assertEqual(list(f.field.choices), [
('null', 'NULL'),
])
def test_default_field_with_queryset(self):
qs = mock.NonCallableMock(spec=[])
f = ModelMultipleChoiceFilter(queryset=qs)
@ -847,6 +903,20 @@ class NumericRangeFilterTests(TestCase):
f.filter(qs, value)
qs.filter.assert_called_once_with(None__exact=(0, 0))
def test_filtering_startswith(self):
qs = mock.Mock(spec=['filter'])
value = mock.Mock(start=20, stop=None)
f = NumericRangeFilter()
f.filter(qs, value)
qs.filter.assert_called_once_with(None__startswith=20)
def test_filtering_endswith(self):
qs = mock.Mock(spec=['filter'])
value = mock.Mock(start=None, stop=30)
f = NumericRangeFilter()
f.filter(qs, value)
qs.filter.assert_called_once_with(None__endswith=30)
class RangeFilterTests(TestCase):
@ -1132,6 +1202,14 @@ class AllValuesFilterTests(TestCase):
field = f.field
self.assertIsInstance(field, forms.ChoiceField)
def test_empty_value_in_choices(self):
f = AllValuesFilter(name='username')
f.model = User
self.assertEqual(list(f.field.choices), [
('', '---------'),
])
class LookupTypesTests(TestCase):
def test_custom_lookup_exprs(self):
@ -1282,7 +1360,7 @@ class OrderingFilterTests(TestCase):
fields=(('a', 'c'), ('b', 'd')),
)
self.assertSequenceEqual(f.field.choices, (
self.assertSequenceEqual(list(f.field.choices), (
('', '---------'),
('a', 'A'),
('b', 'B'),
@ -1293,7 +1371,7 @@ class OrderingFilterTests(TestCase):
fields=(('a', 'c'), ('b', 'd')),
)
self.assertSequenceEqual(f.field.choices, (
self.assertSequenceEqual(list(f.field.choices), (
('', '---------'),
('c', 'C'),
('-c', 'C (descending)'),
@ -1307,7 +1385,7 @@ class OrderingFilterTests(TestCase):
field_labels={'a': 'foo'},
)
self.assertSequenceEqual(f.field.choices, (
self.assertSequenceEqual(list(f.field.choices), (
('', '---------'),
('c', 'foo'),
('-c', 'foo (descending)'),
@ -1324,7 +1402,7 @@ class OrderingFilterTests(TestCase):
}
)
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('username', 'BLABLA'),
('-username', 'XYZXYZ'),
@ -1379,7 +1457,7 @@ class OrderingFilterTests(TestCase):
with translation.override('pl'):
f = OrderingFilter(fields=['username'])
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('username', 'Nazwa użytkownika'),
('-username', 'Nazwa użytkownika (malejąco)'),
@ -1392,8 +1470,13 @@ class OrderingFilterTests(TestCase):
field_labels={'username': 'BLABLA'},
)
self.assertEqual(f.field.choices, [
self.assertEqual(list(f.field.choices), [
('', '---------'),
('username', 'BLABLA'),
('-username', 'BLABLA (malejąco)'),
])
def test_help_text(self):
# regression test for #756 - the ususal CSV help_text is not relevant to ordering filters.
self.assertEqual(OrderingFilter().field.help_text, '')
self.assertEqual(OrderingFilter(help_text='a').field.help_text, 'a')

View File

@ -4,43 +4,46 @@ import mock
import unittest
import django
from django.core.exceptions import ValidationError
from django.db import models
from django.test import TestCase, override_settings
from django_filters.constants import STRICTNESS
from django_filters.filterset import FilterSet
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS
from django_filters.filters import Filter
from django_filters.filters import BooleanFilter
from django_filters.filters import CharFilter
from django_filters.filters import NumberFilter
from django_filters.filters import ChoiceFilter
from django_filters.filters import ModelChoiceFilter
from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filters import UUIDFilter
from django_filters.filters import BaseInFilter
from django_filters.filters import BaseRangeFilter
from django_filters.filters import DateRangeFilter
from django_filters.filters import FilterMethod
from django_filters.filters import (
BaseInFilter,
BaseRangeFilter,
BooleanFilter,
CharFilter,
ChoiceFilter,
DateRangeFilter,
Filter,
FilterMethod,
ModelChoiceFilter,
ModelMultipleChoiceFilter,
NumberFilter,
UUIDFilter
)
from django_filters.filterset import FILTER_FOR_DBFIELD_DEFAULTS, FilterSet
from django_filters.widgets import BooleanWidget
from .models import User
from .models import AdminUser
from .models import Article
from .models import Book
from .models import Profile
from .models import Comment
from .models import Restaurant
from .models import NetworkSetting
from .models import SubnetMaskField
from .models import Account
from .models import BankAccount
from .models import Node
from .models import DirectedNode
from .models import Worker
from .models import Business
from .models import UUIDTestModel
from .models import (
Account,
AdminUser,
Article,
BankAccount,
Book,
Business,
Comment,
DirectedNode,
NetworkSetting,
Node,
Profile,
Restaurant,
SubnetMaskField,
User,
UUIDTestModel,
Worker
)
def checkItemsEqual(L1, L2):
@ -530,7 +533,7 @@ class FilterSetClassCreationTests(TestCase):
SubnetMaskField: {'filter_class': CharFilter}
}
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask'])
self.assertEqual(list(F.base_filters.keys()), ['ip', 'mask', 'cidr'])
def test_custom_declared_field_no_warning(self):
class F(FilterSet):
@ -571,6 +574,21 @@ class FilterSetClassCreationTests(TestCase):
list(F.base_filters) + ['amount_saved'],
list(FtiF.base_filters))
def test_declared_filter_disabling(self):
class Parent(FilterSet):
f1 = CharFilter()
f2 = CharFilter()
class Child(Parent):
f1 = None
class Grandchild(Child):
pass
self.assertEqual(len(Parent.base_filters), 2)
self.assertEqual(len(Child.base_filters), 1)
self.assertEqual(len(Grandchild.base_filters), 1)
class FilterSetInstantiationTests(TestCase):
@ -679,11 +697,15 @@ class FilterSetTogetherTests(TestCase):
('username', 'status'),
('first_name', 'is_active'),
]
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F({}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex'}, queryset=self.qs)
self.assertEqual(f.qs.count(), 0)
with self.assertRaises(ValidationError):
f.qs.count()
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
self.assertEqual(f.qs.count(), 1)
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
@ -694,15 +716,31 @@ class FilterSetTogetherTests(TestCase):
model = User
fields = ['username', 'status']
together = ['username', 'status']
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F({}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex'}, queryset=self.qs)
self.assertEqual(f.qs.count(), 0)
with self.assertRaises(ValidationError):
f.qs.count()
f = F({'username': 'alex', 'status': 1}, queryset=self.qs)
self.assertEqual(f.qs.count(), 1)
self.assertQuerysetEqual(f.qs, [self.alex.pk], lambda o: o.pk)
def test_empty_values(self):
class F(FilterSet):
class Meta:
model = User
fields = ['username', 'status']
together = ['username', 'status']
f = F({'username': '', 'status': ''}, queryset=self.qs)
self.assertEqual(f.qs.count(), 2)
f = F({'username': 'alex', 'status': ''}, queryset=self.qs)
self.assertEqual(f.qs.count(), 0)
# test filter.method here, as it depends on its parent FilterSet
class FilterMethodTests(TestCase):

View File

@ -1,16 +1,12 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from django import forms
from django.test import TestCase, override_settings
from django_filters.filters import CharFilter, ChoiceFilter
from django_filters.filterset import FilterSet
from django_filters.filters import CharFilter
from django_filters.filters import ChoiceFilter
from .models import User, ManagerGroup
from .models import Book
from .models import STATUS_CHOICES, REGULAR, MANAGER
from .models import MANAGER, REGULAR, STATUS_CHOICES, Book, ManagerGroup, User
class FilterSetFormTests(TestCase):
@ -67,7 +63,7 @@ class FilterSetFormTests(TestCase):
self.assertEqual(len(f.fields), 1)
self.assertIn('status', f.fields)
self.assertSequenceEqual(
f.fields['status'].choices,
list(f.fields['status'].choices),
(('', '---------'), ) + STATUS_CHOICES
)
@ -120,7 +116,7 @@ class FilterSetFormTests(TestCase):
self.assertIn('status', f.fields)
self.assertIn('username', f.fields)
self.assertSequenceEqual(
f.fields['status'].choices,
list(f.fields['status'].choices),
STATUS_CHOICES
)
self.assertIsInstance(f.fields['status'].widget, forms.RadioSelect)

View File

@ -1,24 +1,38 @@
import datetime
import unittest
import django
from django.test import TestCase, override_settings
from django.db import models
from django.db.models.constants import LOOKUP_SEP
from django.db.models.fields.related import ForeignObjectRel
from django.forms import ValidationError
from django.test import TestCase, override_settings
from django.utils.functional import Promise
from django.utils.timezone import get_default_timezone
from django_filters.utils import (
get_field_parts, get_model_field, resolve_field,
verbose_field_name, verbose_lookup_expr, label_for_filter
)
from django_filters import STRICTNESS, FilterSet
from django_filters.exceptions import FieldLookupError
from django_filters.utils import (
get_field_parts,
get_model_field,
handle_timezone,
label_for_filter,
raw_validation,
resolve_field,
verbose_field_name,
verbose_lookup_expr
)
from .models import User
from .models import Article
from .models import Book
from .models import HiredWorker
from .models import Business
from .models import (
Account,
Article,
Book,
Business,
Company,
HiredWorker,
NetworkSetting,
User
)
class GetFieldPartsTests(TestCase):
@ -225,6 +239,10 @@ class VerboseFieldNameTests(TestCase):
verbose_name = verbose_field_name(Article, 'name')
self.assertEqual(verbose_name, 'title')
def test_field_all_caps(self):
verbose_name = verbose_field_name(NetworkSetting, 'cidr')
self.assertEqual(verbose_name, 'CIDR')
def test_forwards_related_field(self):
verbose_name = verbose_field_name(Article, 'author__username')
self.assertEqual(verbose_name, 'author username')
@ -233,6 +251,10 @@ class VerboseFieldNameTests(TestCase):
verbose_name = verbose_field_name(Book, 'lovers__first_name')
self.assertEqual(verbose_name, 'lovers first name')
def test_backwards_related_field_multi_word(self):
verbose_name = verbose_field_name(User, 'manager_of')
self.assertEqual(verbose_name, 'manager of')
def test_lazy_text(self):
# sanity check
field = User._meta.get_field('username')
@ -241,6 +263,26 @@ class VerboseFieldNameTests(TestCase):
verbose_name = verbose_field_name(User, 'username')
self.assertEqual(verbose_name, 'username')
def test_forwards_fk(self):
verbose_name = verbose_field_name(Article, 'author')
self.assertEqual(verbose_name, 'author')
def test_backwards_fk(self):
# https://github.com/carltongibson/django-filter/issues/716
# related_name is set
verbose_name = verbose_field_name(Company, 'locations')
self.assertEqual(verbose_name, 'locations')
# related_name not set. Auto-generated relation is `article_set`
# _meta.get_field raises FieldDoesNotExist
verbose_name = verbose_field_name(User, 'article_set')
self.assertEqual(verbose_name, '[invalid name]')
# WRONG NAME! Returns ManyToOneRel with related_name == None.
verbose_name = verbose_field_name(User, 'article')
self.assertEqual(verbose_name, '[invalid name]')
class VerboseLookupExprTests(TestCase):
@ -285,3 +327,42 @@ class LabelForFilterTests(TestCase):
def test_exact_lookup(self):
label = label_for_filter(Article, 'name', 'exact')
self.assertEqual(label, 'Title')
def test_field_all_caps(self):
label = label_for_filter(NetworkSetting, 'cidr', 'contains', exclude=True)
self.assertEqual(label, 'Exclude CIDR contains')
class HandleTimezone(TestCase):
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_handle_dst_ending(self):
dst_ending_date = datetime.datetime(2017, 2, 18, 23, 59, 59, 999999)
handled = handle_timezone(dst_ending_date, False)
self.assertEqual(handled, get_default_timezone().localize(dst_ending_date, False))
@unittest.skipIf(django.VERSION < (1, 9), 'version doesnt supports is_dst parameter for make_aware')
@override_settings(TIME_ZONE='America/Sao_Paulo')
def test_handle_dst_starting(self):
dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0)
handled = handle_timezone(dst_starting_date, True)
self.assertEqual(handled, get_default_timezone().localize(dst_starting_date, True))
class RawValidationDataTests(TestCase):
def test_simple(self):
class F(FilterSet):
class Meta:
model = Article
fields = ['id', 'author', 'name']
strict = STRICTNESS.RAISE_VALIDATION_ERROR
f = F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'})
with self.assertRaises(ValidationError) as exc:
f.qs
self.assertDictEqual(raw_validation(exc.exception), {
'id': ['Enter a number.'],
'author': ['Select a valid choice. That choice is not one of the available choices.'],
})

View File

@ -1,12 +1,11 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from django.test.client import RequestFactory
from django_filters.views import FilterView
from django_filters.filterset import FilterSet, filterset_factory
from django_filters.views import FilterView
from .models import Book
@ -47,6 +46,33 @@ class GenericClassBasedViewTests(GenericViewTestCase):
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_with_model_no_filterset(self):
factory = RequestFactory()
request = factory.get(self.base_url)
view = FilterView.as_view(model=Book)
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_with_model_and_fields_no_filterset(self):
factory = RequestFactory()
request = factory.get(self.base_url + '?price=1.0')
view = FilterView.as_view(model=Book, filter_fields=['price'])
# filtering only by price
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
# not filtering by title
request = factory.get(self.base_url + '?title=Snowcrash')
response = view(request)
self.assertEqual(response.status_code, 200)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
def test_view_without_filterset_or_model(self):
factory = RequestFactory()
request = factory.get(self.base_url)
@ -72,6 +98,9 @@ class GenericFunctionalViewTests(GenericViewTestCase):
response = self.client.get(self.base_url)
for b in ['Ender&#39;s Game', 'Rainbow Six', 'Snowcrash']:
self.assertContains(response, b)
# extra context
self.assertEqual(response.context_data['foo'], 'bar')
self.assertEqual(response.context_data['bar'], 'foo')
def test_view_filtering_on_price(self):
response = self.client.get(self.base_url + '?title=Snowcrash')

View File

@ -1,15 +1,18 @@
from __future__ import absolute_import
from __future__ import unicode_literals
from __future__ import absolute_import, unicode_literals
from django.forms import Select, TextInput
from django.test import TestCase
from django.forms import TextInput, Select
from django_filters.widgets import BooleanWidget, QueryArrayWidget
from django_filters.widgets import BaseCSVWidget
from django_filters.widgets import CSVWidget
from django_filters.widgets import RangeWidget
from django_filters.widgets import LinkWidget
from django_filters.widgets import LookupTypeWidget
from django_filters.widgets import (
BaseCSVWidget,
BooleanWidget,
CSVWidget,
LinkWidget,
LookupTypeWidget,
QueryArrayWidget,
RangeWidget,
SuffixedMultiWidget
)
class LookupTypeWidgetTests(TestCase):
@ -121,6 +124,70 @@ class LinkWidgetTests(TestCase):
self.assertEqual(result, 'test-val1')
class SuffixedMultiWidgetTests(TestCase):
def test_assertions(self):
# number of widgets must match suffixes
with self.assertRaises(AssertionError):
SuffixedMultiWidget(widgets=[BooleanWidget])
# suffixes must be unique
class W(SuffixedMultiWidget):
suffixes = ['a', 'a']
with self.assertRaises(AssertionError):
W(widgets=[BooleanWidget, BooleanWidget])
# should succeed
class W(SuffixedMultiWidget):
suffixes = ['a', 'b']
W(widgets=[BooleanWidget, BooleanWidget])
def test_render(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']
w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price_min" type="text" />
<input name="price_max" type="text" />
""")
# blank suffix
class W(SuffixedMultiWidget):
suffixes = [None, 'lookup']
w = W(widgets=[TextInput, TextInput])
self.assertHTMLEqual(w.render('price', ''), """
<input name="price" type="text" />
<input name="price_lookup" type="text" />
""")
def test_value_from_datadict(self):
class W(SuffixedMultiWidget):
suffixes = ['min', 'max']
w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price_min': '1',
'price_max': '2',
}, {}, 'price')
self.assertEqual(result, ['1', '2'])
result = w.value_from_datadict({}, {}, 'price')
self.assertEqual(result, [None, None])
# blank suffix
class W(SuffixedMultiWidget):
suffixes = ['', 'lookup']
w = W(widgets=[TextInput, TextInput])
result = w.value_from_datadict({
'price': '1',
'price_lookup': 'lt',
}, {}, 'price')
self.assertEqual(result, ['1', 'lt'])
class RangeWidgetTests(TestCase):
def test_widget(self):
@ -183,6 +250,9 @@ class CSVWidgetTests(TestCase):
self.assertHTMLEqual(w.render('price', ''), """
<input type="text" name="price" />""")
self.assertHTMLEqual(w.render('price', []), """
<input type="text" name="price" />""")
self.assertHTMLEqual(w.render('price', '1'), """
<input type="text" name="price" value="1" />""")

View File

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

38
tox.ini
View File

@ -1,38 +0,0 @@
[tox]
envlist =
{py27,py33,py34,py35}-django18-restframework{34,35},
{py27,py34,py35}-django{19,110}-restframework{34,35},
{py27,py34,py35}-djangolatest-restframeworklatest,
warnings
[testenv]
commands = coverage run --source django_filters ./runtests.py {posargs}
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
django18: django>=1.8.0,<1.9.0
django19: django>=1.9.0,<1.10.0
django110: django>=1.10.0,<1.11.0
djangolatest: https://github.com/django/django/archive/master.tar.gz
restframework34: djangorestframework>=3.4,<3.5
restframework35: djangorestframework>=3.5,<3.6
restframeworklatest: https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
-rrequirements/test-ci.txt
[testenv:py27-djangolatest-restframeworklatest]
ignore_outcome = True
[testenv:py34-djangolatest-restframeworklatest]
ignore_outcome = True
[testenv:py35-djangolatest-restframeworklatest]
ignore_outcome = True
[testenv:warnings]
ignore_outcome = True
commands = python -Werror ./runtests.py {posargs}
deps =
https://github.com/django/django/archive/master.tar.gz
https://github.com/tomchristie/django-rest-framework/archive/master.tar.gz
-rrequirements/test-ci.txt