1Django Rest Framework Filters
2=============================
3
4.. image:: https://travis-ci.org/philipn/django-rest-framework-filters.png?branch=master
5 :target: https://travis-ci.org/philipn/django-rest-framework-filters
6
7.. image:: https://codecov.io/gh/philipn/django-rest-framework-filters/branch/master/graph/badge.svg
8 :target: https://codecov.io/gh/philipn/django-rest-framework-filters
9
10.. image:: https://img.shields.io/pypi/v/djangorestframework-filters.svg
11 :target: https://pypi.python.org/pypi/djangorestframework-filters
12
13
14``django-rest-framework-filters`` is an extension to `Django REST framework`_ and `Django filter`_
15that makes it easy to filter across relationships. Historically, this extension also provided a
16number of additional features and fixes, however the number of features has shrunk as they are
17merged back into ``django-filter``.
18
19.. _`Django REST framework`: https://github.com/tomchristie/django-rest-framework
20.. _`Django filter`: https://github.com/carltongibson/django-filter
21
22Using ``django-rest-framework-filters``, we can easily do stuff like::
23
24 /api/article?author__first_name__icontains=john
25 /api/article?is_published!=true
26
27.. contents::
28 **Table of Contents**
29 :local:
30 :depth: 2
31 :backlinks: none
32
33Features
34--------
35
36* Easy filtering across relationships
37* Support for method filtering across relationships
38* Automatic filter negation with a simple ``param!=value`` syntax
39* Backend caching to increase performance
40
41
42Requirements
43------------
44
45* **Python**: 2.7 or 3.3+
46* **Django**: 1.8, 1.9, 1.10, 1.11
47* **DRF**: 3.5, 3.6
48
49
50Installation
51------------
52
53.. code-block:: bash
54
55 $ pip install djangorestframework-filters
56
57
58Usage
59-----
60
61Upgrading from ``django-filter`` to ``django-rest-framework-filters`` is straightforward:
62
63* Import from ``rest_framework_filters`` instead of from ``django_filters``
64* Use the ``rest_framework_filters`` backend instead of the one provided by ``django_filter``.
65
66.. code-block:: python
67
68 # django-filter
69 from django_filters.rest_framework import FilterSet, filters
70
71 class ProductFilter(FilterSet):
72 manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all())
73 ...
74
75
76 # django-rest-framework-filters
77 import rest_framework_filters as filters
78
79 class ProductFilter(filters.FilterSet):
80 manufacturer = filters.ModelChoiceFilter(queryset=Manufacturer.objects.all())
81 ...
82
83
84To use the django-rest-framework-filters backend, add the following to your settings:
85
86.. code-block:: python
87
88 REST_FRAMEWORK = {
89 'DEFAULT_FILTER_BACKENDS': (
90 'rest_framework_filters.backends.DjangoFilterBackend', ...
91 ),
92 ...
93
94
95Once configured, you can continue to use all of the filters found in ``django-filter``.
96
97
98Filtering across relationships
99~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100
101You can easily traverse multiple relationships when filtering by using ``RelatedFilter``:
102
103.. code-block:: python
104
105 from rest_framework import viewsets
106 import rest_framework_filters as filters
107
108
109 class ManagerFilter(filters.FilterSet):
110 class Meta:
111 model = Manager
112 fields = {'name': ['exact', 'in', 'startswith']}
113
114
115 class DepartmentFilter(filters.FilterSet):
116 manager = filters.RelatedFilter(ManagerFilter, name='manager', queryset=Manager.objects.all())
117
118 class Meta:
119 model = Department
120 fields = {'name': ['exact', 'in', 'startswith']}
121
122
123 class CompanyFilter(filters.FilterSet):
124 department = filters.RelatedFilter(DepartmentFilter, name='department', queryset=Department.objects.all())
125
126 class Meta:
127 model = Company
128 fields = {'name': ['exact', 'in', 'startswith']}
129
130
131 # company viewset
132 class CompanyView(viewsets.ModelViewSet):
133 filter_class = CompanyFilter
134 ...
135
136Example filter calls:
137
138.. code-block::
139
140 /api/companies?department__name=Accounting
141 /api/companies?department__manager__name__startswith=Bob
142
143``queryset`` callables
144""""""""""""""""""""""
145
146Since ``RelatedFilter`` is a subclass of ``ModelChoiceFilter``, the ``queryset`` argument supports callable behavior.
147In the following example, the set of departments is restricted to those in the user's company.
148
149.. code-block:: python
150
151 def departments(request):
152 company = request.user.company
153 return company.department_set.all()
154
155 class EmployeeFilter(filters.FilterSet):
156 department = filters.RelatedFilter(filterset=DepartmentFilter, queryset=departments)
157 ...
158
159Recursive relationships
160"""""""""""""""""""""""
161
162Recursive relations are also supported. It may be necessary to specify the full module path.
163
164.. code-block:: python
165
166 class PersonFilter(filters.FilterSet):
167 name = filters.AllLookupsFilter(name='name')
168 best_friend = filters.RelatedFilter('people.views.PersonFilter', name='best_friend', queryset=Person.objects.all())
169
170 class Meta:
171 model = Person
172
173Supporting ``Filter.method``
174~~~~~~~~~~~~~~~~~~~~~~~~~~~~
175
176``django_filters.MethodFilter`` has been deprecated and reimplemented as the ``method`` argument
177to all filter classes. It incorporates some of the implementation details of the old
178``rest_framework_filters.MethodFilter``, but requires less boilerplate and is simpler to write.
179
180* It is no longer necessary to perform empty/null value checking.
181* You may use any filter class (``CharFilter``, ``BooleanFilter``, etc...) which will
182 validate input values for you.
183* The argument signature has changed from ``(name, qs, value)`` to ``(qs, name, value)``.
184
185.. code-block:: python
186
187 class PostFilter(filters.FilterSet):
188 # Note the use of BooleanFilter, the original model field's name, and the method argument.
189 is_published = filters.BooleanFilter(name='date_published', method='filter_is_published')
190
191 class Meta:
192 model = Post
193 fields = ['title', 'content']
194
195 def filter_is_published(self, qs, name, value):
196 """
197 `is_published` is based on the `date_published` model field.
198 If the publishing date is null, then the post is not published.
199 """
200 # incoming value is normalized as a boolean by BooleanFilter
201 isnull = not value
202 lookup_expr = LOOKUP_SEP.join([name, 'isnull'])
203
204 return qs.filter(**{lookup_expr: isnull})
205
206 class AuthorFilter(filters.FilterSet):
207 posts = filters.RelatedFilter('PostFilter', queryset=Post.objects.all())
208
209 class Meta:
210 model = Author
211 fields = ['name']
212
213The above would enable the following filter calls:
214
215.. code-block::
216
217 /api/posts?is_published=true
218 /api/authors?posts__is_published=true
219
220
221In the first API call, the filter method receives a queryset of posts. In the second,
222it receives a queryset of users. The filter method in the example modifies the lookup
223name to work across the relationship, allowing you to find published posts, or authors
224who have published posts.
225
226Automatic Filter Negation/Exclusion
227~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
228
229FilterSets support automatic exclusion using a simple ``param!=value`` syntax. This syntax
230internally sets the ``exclude`` property on the filter.
231
232.. code-block::
233
234 /api/page?title!=The%20Park
235
236This syntax supports regular filtering combined with exclusion filtering. For example, the
237following would search for all articles containing "Hello" in the title, while excluding
238those containing "World".
239
240.. code-block::
241
242 /api/articles?title__contains=Hello&title__contains!=World
243
244Note that most filters only accept a single query parameter. In the above, ``title__contains``
245and ``title__contains!`` are interpreted as two separate query parameters. The following would
246probably be invalid, although it depends on the specifics of the individual filter class:
247
248.. code-block::
249
250 /api/articles?title__contains=Hello&title__contains!=World&title_contains!=Friend
251
252
253Allowing any lookup type on a field
254~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
255
256If you need to enable several lookups for a field, django-filter provides the dict-syntax for
257``Meta.fields``.
258
259.. code-block:: python
260
261 class ProductFilter(filters.FilterSet):
262 class Meta:
263 model = Product
264 fields = {
265 'price': ['exact', 'lt', 'gt', ...],
266 }
267
268``django-rest-framework-filters`` also allows you to enable all possible lookups for any field.
269This can be achieved through the use of ``AllLookupsFilter`` or using the ``'__all__'`` value in
270the ``Meta.fields`` dict-style syntax. Generated filters (``Meta.fields``, ``AllLookupsFilter``)
271will never override your declared filters.
272
273Note that using all lookups comes with the same admonitions as enabling ``'__all__'`` fields in
274django forms (`docs`_). Exposing all lookups may allow users to construct queries that
275inadvertently leak data. Use this feature responsibly.
276
277.. _`docs`: https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#selecting-the-fields-to-use
278
279.. code-block:: python
280
281 class ProductFilter(filters.FilterSet):
282 # Not overridden by `__all__`
283 price__gt = filters.NumberFilter(name='price', lookup_expr='gt', label='Minimum price')
284
285 class Meta:
286 model = Product
287 fields = {
288 'price': '__all__',
289 }
290
291 # or
292
293 class ProductFilter(filters.FilterSet):
294 price = filters.AllLookupsFilter()
295
296 # Not overridden by `AllLookupsFilter`
297 price__gt = filters.NumberFilter(name='price', lookup_expr='gt', label='Minimum price')
298
299 class Meta:
300 model = Product
301
302You cannot combine ``AllLookupsFilter`` with ``RelatedFilter`` as the filter names would clash.
303
304.. code-block:: python
305
306 class ProductFilter(filters.FilterSet):
307 manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all())
308 manufacturer = filters.AllLookupsFilter()
309
310To work around this, you have the following options:
311
312.. code-block:: python
313
314 class ProductFilter(filters.FilterSet):
315 manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all())
316
317 class Meta:
318 model = Product
319 fields = {
320 'manufacturer': '__all__',
321 }
322
323 # or
324
325 class ProductFilter(filters.FilterSet):
326 manufacturer = filters.RelatedFilter('ManufacturerFilter', queryset=Manufacturer.objects.all(), lookups='__all__') # `lookups` also accepts a list
327
328 class Meta:
329 model = Product
330
331
332Can I mix and match ``django-filter`` and ``django-rest-framework-filters``?
333~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
334
335Yes you can. ``django-rest-framework-filters`` is simply an extension of ``django-filter``. Note
336that ``RelatedFilter`` and other ``django-rest-framework-filters`` features are designed to work
337with ``rest_framework_filters.FilterSet`` and will not function on a ``django_filters.FilterSet``.
338However, the target ``RelatedFilter.filterset`` may point to a ``FilterSet`` from either package,
339and both ``FilterSet`` implementations are compatible with the other's DRF backend.
340
341.. code-block:: python
342
343 # valid
344 class VanillaFilter(django_filters.FilterSet):
345 ...
346
347 class DRFFilter(rest_framework_filters.FilterSet):
348 vanilla = rest_framework_filters.RelatedFilter(filterset=VanillaFilter, queryset=...)
349
350
351 # invalid
352 class DRFFilter(rest_framework_filters.FilterSet):
353 ...
354
355 class VanillaFilter(django_filters.FilterSet):
356 drf = rest_framework_filters.RelatedFilter(filterset=DRFFilter, queryset=...)
357
358
359Caveats & Limitations
360~~~~~~~~~~~~~~~~~~~~~
361
362``MultiWidget`` is incompatible
363"""""""""""""""""""""""""""""""
364
365djangorestframework-filters is not compatible with form widgets that parse query names that differ from the filter's
366attribute name. Although this only practically applies to ``MultiWidget``, it is a general limitation that affects
367custom widgets that also have this behavior. Affected filters include ``RangeFilter``, ``DateTimeFromToRangeFilter``,
368``DateFromToRangeFilter``, ``TimeRangeFilter``, and ``NumericRangeFilter``.
369
370To demonstrate the incompatiblity, take the following filterset:
371
372.. code-block:: python
373
374 class PostFilter(FilterSet):
375 publish_date = filters.DateFromToRangeFilter()
376
377The above filter allows users to perform a ``range`` query on the publication date. The filter class internally uses
378``MultiWidget`` to separately parse the upper and lower bound values. The incompatibility lies in that ``MultiWidget``
379appends an index to its inner widget names. Instead of parsing ``publish_date``, it expects ``publish_date_0`` and
380``publish_date_1``. It is possible to fix this by including the attribute name in the querystring, although this is
381not recommended.
382
383.. code-block::
384
385 ?publish_date_0=2016-01-01&publish_date_1=2016-02-01&publish_date=
386
387``MultiWidget`` is also discouraged since:
388
389* ``core-api`` field introspection fails for similar reasons
390* ``_0`` and ``_1`` are less API-friendly than ``_min`` and ``_max``
391
392The recommended solutions are to either:
393
394* Create separate filters for each of the sub-widgets (such as ``publish_date_min`` and ``publish_date_max``).
395* Use a CSV-based filter such as those derived from ``BaseCSVFilter``/``BaseInFilter``/``BaseRangeFilter``. eg,
396
397.. code-block::
398
399 ?publish_date__range=2016-01-01,2016-02-01
400
401
402Migrating to 1.0
403----------------
404
405``RelatedFilter.queryset`` now required
406~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
407
408The related filterset's model is no longer used to provide the default value for ``RelatedFilter.queryset``. This
409change reduces the chance of unintentionally exposing data in the rendered filter forms. You must now explicitly
410provide the ``queryset`` argument, or override the ``get_queryset()`` method (see `queryset callables`_).
411
412
413``get_filters()`` renamed to ``expand_filters()``
414~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
415
416django-filter has add a ``get_filters()`` classmethod to it's API, so this method has been renamed.
417
418
419Publishing
420----------
421
422.. code-block:: bash
423
424 $ pip install -U twine setuptools wheel
425 $ rm -rf dist/ build/
426 $ python setup.py sdist bdist_wheel
427 $ twine upload dist/*
428
429
430License
431-------
432Copyright (c) 2013-2015 Philip Neustrom <philipn@gmail.com>,
4332016-2017 Ryan P Kilby <rpkilby@ncsu.edu>
434
435Permission is hereby granted, free of charge, to any person obtaining a copy
436of this software and associated documentation files (the "Software"), to deal
437in the Software without restriction, including without limitation the rights
438to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
439copies of the Software, and to permit persons to whom the Software is
440furnished to do so, subject to the following conditions:
441
442The above copyright notice and this permission notice shall be included in
443all copies or substantial portions of the Software.
444
445THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
446IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
447FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
448AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
449LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
450OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
451THE SOFTWARE.
452