Skip to content

Commit 677b48f

Browse files
author
Carlton Gibson
authored
Merge pull request carltongibson#554 from rpkilby/docs-changes
Documentation updates
2 parents 4d02cdf + 2159d90 commit 677b48f

16 files changed

+366
-249
lines changed

README.rst

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ Full documentation on `read the docs`_.
1212
Requirements
1313
------------
1414

15-
* Python 2.7, 3.3, 3.4, 3.5
16-
* Django 1.8, 1.9, 1.10
17-
* DRF 3.3 (Django 1.8 only), 3.4
15+
* **Python**: 2.7, 3.3, 3.4, 3.5
16+
* **Django**: 1.8, 1.9, 1.10
17+
* **DRF**: 3.4, 3.5
1818

1919
Installation
2020
------------
File renamed without changes.

docs/dev/tests.txt

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
======================
2+
Running the Test Suite
3+
======================
4+
5+
The easiest way to run the django-filter tests is to check out the source
6+
code and create a virtualenv where you can install the test dependencies.
7+
Django-filter uses a custom test runner to configure the environment, so a
8+
wrapper script is available to set up and run the test suite.
9+
10+
.. note::
11+
12+
The following assumes you have `virtualenv`__ and `git`__ installed.
13+
14+
__ https://virtualenv.pypa.io/en/stable/
15+
__ https://git-scm.com
16+
17+
Clone the repository
18+
--------------------
19+
20+
Get the source code using the following command:
21+
22+
.. code-block:: bash
23+
24+
$ git clone https://github.com/carltongibson/django-filter.git
25+
26+
Switch to the django-filter directory:
27+
28+
.. code-block:: bash
29+
30+
$ cd django-filter
31+
32+
Set up the virtualenv
33+
---------------------
34+
35+
Create a new virtualenv to run the test suite in:
36+
37+
.. code-block:: bash
38+
39+
$ virtualenv venv
40+
41+
Then activate the virtualenv and install the test requirements:
42+
43+
.. code-block:: bash
44+
45+
$ source venv/bin/activate
46+
$ pip install -r requirements/test.txt
47+
48+
Execute the test runner
49+
-----------------------
50+
51+
Run the tests with the runner script:
52+
53+
.. code-block:: bash
54+
55+
$ python runtests.py
56+
57+
58+
Test all supported versions
59+
---------------------------
60+
61+
You can also use the excellent tox testing tool to run the tests against all
62+
supported versions of Python and Django. Install tox, and then simply run:
63+
64+
.. code-block:: bash
65+
66+
$ pip install tox
67+
$ tox

docs/guide/install.txt

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
============
2+
Installation
3+
============
4+
5+
Django-filter can be installed from PyPI with tools like ``pip``:
6+
7+
.. code-block:: bash
8+
9+
$ pip install django-filter
10+
11+
Then add ``'django_filters'`` to your ``INSTALLED_APPS``.
12+
13+
.. note::
14+
15+
django-filter provides *some* localization for *some* languages. If you do
16+
not need these translations (or would rather provide your own), then it is
17+
unnecessary to add django-filter to the ``INSTALLED_APPS`` setting.
18+
19+
20+
Requirements
21+
------------
22+
23+
Django-filter is tested against all supported versions of Python and `Django`__,
24+
as well as the latest versions of Django REST Framework (`DRF`__).
25+
26+
__ https://www.djangoproject.com/download/
27+
__ http://www.django-rest-framework.org/
28+
29+
30+
31+
* **Python**: 2.7, 3.3, 3.4, 3.5
32+
* **Django**: 1.8, 1.9, 1.10
33+
* **DRF**: 3.4, 3.5

docs/migration.txt renamed to docs/guide/migration.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
================
12
Migrating to 1.0
23
================
34

docs/rest_framework.txt renamed to docs/guide/rest_framework.txt

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
Django Rest Framework
2-
=====================
1+
====================
2+
Integration with DRF
3+
====================
34

45
Integration with `Django Rest Framework`__ is provided through a DRF-specific ``FilterSet`` and a `filter backend`__. These may be found in the ``rest_framework`` sub-package.
56

@@ -72,8 +73,8 @@ To enable filtering with a ``FilterSet``, add it to the ``filter_class`` paramet
7273
filter_class = ProductFilter
7374

7475

75-
Specifying ``filter_fields``
76-
----------------------------
76+
Using the ``filter_fields`` shortcut
77+
------------------------------------
7778

7879
You may bypass creating a ``FilterSet`` by instead adding ``filter_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields <fields>`.
7980

@@ -168,4 +169,4 @@ If you are using DRF's browsable API or admin API you may also want to install `
168169

169170
With crispy forms installed and added to Django's ``INSTALLED_APPS``, the browsable API will present a filtering control for ``DjangoFilterBackend``, like so:
170171

171-
.. image:: img/form.png
172+
.. image:: ../assets/form.png

docs/guide/tips.txt

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
==================
2+
Tips and Solutions
3+
==================
4+
5+
Common problems for declared filters
6+
------------------------------------
7+
8+
Below are some of the common problem that occur when declaring filters. It is
9+
recommended that you read this as it provides a more complete understanding of
10+
how filters work.
11+
12+
13+
Filter ``name`` and ``lookup_expr`` not configured
14+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15+
16+
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
17+
them. By default, if ``name`` is not specified, the filter's name on the
18+
filterset class will be used. Additionally, ``lookup_expr`` defaults to
19+
``exact``. The following is an example of a misconfigured price filter:
20+
21+
.. code-block:: python
22+
23+
class ProductFilter(django_filters.FilterSet):
24+
price__gt = django_filters.NumberFilter()
25+
26+
The filter instance will have a field name of ``price__gt`` and an ``exact``
27+
lookup type. Under the hood, this will incorrectly be resolved as:
28+
29+
.. code-block:: python
30+
31+
Produce.objects.filter(price__gt__exact=value)
32+
33+
The above will most likely generate a ``FieldError``. The correct configuration
34+
would be:
35+
36+
.. code-block:: python
37+
38+
class ProductFilter(django_filters.FilterSet):
39+
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
40+
41+
42+
Missing ``lookup_expr`` for text search filters
43+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
44+
45+
It's quite common to forget to set the lookup expression for :code:`CharField`
46+
and :code:`TextField` and wonder why a search for "foo" does not return results
47+
for "foobar". This is because the default lookup type is ``exact``, but you
48+
probably want to perform an ``icontains`` lookup.
49+
50+
51+
Filter and lookup expression mismatch (in, range, isnull)
52+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
53+
54+
It's not always appropriate to directly match a filter to its model field's
55+
type, as some lookups expect different types of values. This is a commonly
56+
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
57+
at the following product model:
58+
59+
.. code-block:: python
60+
61+
class Product(models.Model):
62+
category = models.ForeignKey(Category, null=True)
63+
64+
Given that ``category`` is optional, it's reasonable to want to enable a search
65+
for uncategorized products. The following is an incorrectly configured
66+
``isnull`` filter:
67+
68+
.. code-block:: python
69+
70+
class ProductFilter(django_filters.FilterSet):
71+
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
72+
73+
So what's the issue? While the underlying column type for ``category`` is an
74+
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
75+
only validates numbers. Filters are not `'expression aware'` and won't change
76+
behavior based on their ``lookup_expr``. You should use filters that match the
77+
data type of the lookup expression `instead` of the data type underlying the
78+
model field. The following would correctly allow you to search for both
79+
uncategorized products and products for a set of categories:
80+
81+
.. code-block:: python
82+
83+
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
84+
pass
85+
86+
class ProductFilter(django_filters.FilterSet):
87+
categories = NumberInFilter(name='category', lookup_expr='in')
88+
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
89+
90+
More info on constructing ``in`` and ``range`` csv :ref:`filters <base-in-filter>`.
91+
92+
93+
Filtering by empty values
94+
-------------------------
95+
96+
There are a number of cases where you may need to filter by empty or null
97+
values. The following are some common solutions to these problems:
98+
99+
100+
Filtering by null values
101+
~~~~~~~~~~~~~~~~~~~~~~~~
102+
103+
As explained in the above "Filter and lookup expression mismatch" section, a
104+
common problem is how to correctly filter by null values on a field.
105+
106+
Solution 1: Using a ``BooleanFilter`` with ``isnull``
107+
"""""""""""""""""""""""""""""""""""""""""""""""""""""
108+
109+
Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by
110+
the FilterSet's automatic filter generation. To do this manually, simply add:
111+
112+
.. code-block:: python
113+
114+
class ProductFilter(django_filters.FilterSet):
115+
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
116+
117+
.. note::
118+
119+
Remember that the filter class is validating the input value. The underlying
120+
type of the mode field is not relevant here.
121+
122+
You may also reverse the logic with the ``exclude`` parameter.
123+
124+
.. code-block:: python
125+
126+
class ProductFilter(django_filters.FilterSet):
127+
has_category = django_filters.BooleanFilter(name='category', lookup_expr='isnull', exclude=True)
128+
129+
Solution 2: Using ``ChoiceFilter``'s null choice
130+
""""""""""""""""""""""""""""""""""""""""""""""""
131+
132+
If you're using a ChoiceFilter, you may also filter by null values by enabling
133+
the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
134+
:ref:`docs <choice-filter>`.
135+
136+
.. code-block:: python
137+
138+
class ProductFilter(django_filters.FilterSet):
139+
category = django_filters.ModelChoiceFilter(
140+
name='category', lookup_expr='isnull',
141+
null_label='Uncategorized',
142+
queryset=Category.objects.all(),
143+
)
144+
145+
Solution 3: Combining fields w/ ``MultiValueField``
146+
""""""""""""""""""""""""""""""""""""""""""""""""""
147+
148+
An alternative approach is to use Django's ``MultiValueField`` to manually add
149+
in a ``BooleanField`` to handle null values. Proof of concept:
150+
https://github.com/carltongibson/django-filter/issues/446
151+
152+
153+
Filtering by an empty string
154+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
155+
156+
It's not currently possible to filter by an empty string, since empty values are
157+
interpreted as a skipped filter.
158+
159+
.. code-block:: http
160+
161+
GET http://localhost/api/my-model?myfield=
162+
163+
Solution 1: Magic values
164+
""""""""""""""""""""""""
165+
166+
You can override the ``filter()`` method of a filter class to specifically check
167+
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
168+
169+
.. code-block:: http
170+
171+
GET http://localhost/api/my-model?myfield=EMPTY
172+
173+
.. code-block:: python
174+
175+
class MyCharFilter(filters.CharFilter):
176+
empty_value = 'EMPTY'
177+
178+
def filter(self, qs, value):
179+
if value != self.empty_value:
180+
return super(MyCharFilter, self).filter(qs, value)
181+
182+
qs = self.get_method(qs)(**{'%s__%s' % (self.name, self.lookup_expr): ""})
183+
return qs.distinct() if self.distinct else qs
184+
185+
186+
Solution 2: Empty string filter
187+
"""""""""""""""""""""""""""""""
188+
189+
It would also be possible to create an empty value filter that exhibits the same
190+
behavior as an ``isnull`` filter.
191+
192+
.. code-block:: http
193+
194+
GET http://localhost/api/my-model?myfield__isempty=false
195+
196+
.. code-block:: python
197+
198+
from django.core.validators import EMPTY_VALUES
199+
200+
class EmptyStringFilter(filters.BooleanFilter):
201+
def filter(self, qs, value):
202+
if value in EMPTY_VALUES:
203+
return qs
204+
205+
exclude = self.exclude ^ (value is False)
206+
method = qs.exclude if exclude else qs.filter
207+
208+
return method(**{self.name: ""})
209+
210+
211+
class MyFilterSet(filters.FilterSet):
212+
myfield__isempty = EmptyStringFilter(name='myfield')
213+
214+
class Meta:
215+
model = MyModel

0 commit comments

Comments
 (0)