generated from readthedocs/tutorial-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tips.txt
290 lines (191 loc) · 9.93 KB
/
tips.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
==================
Tips and Solutions
==================
Common problems for declared filters
------------------------------------
Below are some of the common problems that occur when declaring filters. It is
recommended that you read this as it provides a more complete understanding of
how filters work.
Filter ``field_name`` and ``lookup_expr`` not configured
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While ``field_name`` and ``lookup_expr`` are optional, it is recommended that you specify
them. By default, if ``field_name`` is not specified, the filter's name on the
``FilterSet`` class will be used. Additionally, ``lookup_expr`` defaults to
``exact``. The following is an example of a misconfigured price filter:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter()
The filter instance will have a field name of ``price__gt`` and an ``exact``
lookup type. Under the hood, this will incorrectly be resolved as:
.. code-block:: python
Product.objects.filter(price__gt__exact=value)
The above will most likely generate a ``FieldError``. The correct configuration
would be:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter(field_name='price', lookup_expr='gt')
Missing ``lookup_expr`` for text search filters
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's quite common to forget to set the lookup expression for :code:`CharField`
and :code:`TextField` and wonder why a search for "foo" does not return results
for "foobar". This is because the default lookup type is ``exact``, but you
probably want to perform an ``icontains`` lookup.
Filter and lookup expression mismatch (in, range, isnull)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's not always appropriate to directly match a filter to its model field's
type, as some lookups expect different types of values. This is a commonly
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
at the following product model:
.. code-block:: python
class Product(models.Model):
category = models.ForeignKey(Category, null=True)
Given that ``category`` is optional, it's reasonable to want to enable a search
for uncategorized products. The following is an incorrectly configured
``isnull`` filter:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
uncategorized = django_filters.NumberFilter(field_name='category', lookup_expr='isnull')
So what's the issue? While the underlying column type for ``category`` is an
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
only validates numbers. Filters are not `'expression aware'` and won't change
behavior based on their ``lookup_expr``. You should use filters that match the
data type of the lookup expression `instead` of the data type underlying the
model field. The following would correctly allow you to search for both
uncategorized products and products for a set of categories:
.. code-block:: python
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
pass
class ProductFilter(django_filters.FilterSet):
categories = NumberInFilter(field_name='category', lookup_expr='in')
uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull')
More info on constructing ``in`` and ``range`` csv :ref:`filters <base-in-filter>`.
Filtering by empty values
-------------------------
There are a number of cases where you may need to filter by empty or null
values. The following are some common solutions to these problems:
Filtering by null values
~~~~~~~~~~~~~~~~~~~~~~~~
As explained in the above "Filter and lookup expression mismatch" section, a
common problem is how to correctly filter by null values on a field.
Solution 1: Using a ``BooleanFilter`` with ``isnull``
"""""""""""""""""""""""""""""""""""""""""""""""""""""
Using ``BooleanFilter`` with an ``isnull`` lookup is a builtin solution used by
the FilterSet's automatic filter generation. To do this manually, simply add:
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
uncategorized = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull')
.. note::
Remember that the filter class is validating the input value. The underlying
type of the mode field is not relevant here.
You may also reverse the logic with the ``exclude`` parameter.
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
has_category = django_filters.BooleanFilter(field_name='category', lookup_expr='isnull', exclude=True)
Solution 2: Using ``ChoiceFilter``'s null choice
""""""""""""""""""""""""""""""""""""""""""""""""
If you're using a ChoiceFilter, you may also filter by null values by enabling
the ``null_label`` parameter. More details in the ``ChoiceFilter`` reference
:ref:`docs <choice-filter>`.
.. code-block:: python
class ProductFilter(django_filters.FilterSet):
category = django_filters.ModelChoiceFilter(
field_name='category', lookup_expr='isnull',
null_label='Uncategorized',
queryset=Category.objects.all(),
)
Solution 3: Combining fields w/ ``MultiValueField``
"""""""""""""""""""""""""""""""""""""""""""""""""""
An alternative approach is to use Django's ``MultiValueField`` to manually add
in a ``BooleanField`` to handle null values. Proof of concept:
https://github.com/carltongibson/django-filter/issues/446
Filtering by an empty string
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's not currently possible to filter by an empty string, since empty values are
interpreted as a skipped filter.
GET http://localhost/api/my-model?myfield=
Solution 1: Magic values
""""""""""""""""""""""""
You can override the ``filter()`` method of a filter class to specifically check
for magic values. This is similar to the ``ChoiceFilter``'s null value handling.
GET http://localhost/api/my-model?myfield=EMPTY
.. code-block:: python
class MyCharFilter(filters.CharFilter):
empty_value = 'EMPTY'
def filter(self, qs, value):
if value != self.empty_value:
return super().filter(qs, value)
qs = self.get_method(qs)(**{'%s__%s' % (self.field_name, self.lookup_expr): ""})
return qs.distinct() if self.distinct else qs
Solution 2: Empty string filter
"""""""""""""""""""""""""""""""
It would also be possible to create an empty value filter that exhibits the same
behavior as an ``isnull`` filter.
GET http://localhost/api/my-model?myfield__isempty=false
.. code-block:: python
from django.core.validators import EMPTY_VALUES
class EmptyStringFilter(filters.BooleanFilter):
def filter(self, qs, value):
if value in EMPTY_VALUES:
return qs
exclude = self.exclude ^ (value is False)
method = qs.exclude if exclude else qs.filter
return method(**{self.field_name: ""})
class MyFilterSet(filters.FilterSet):
myfield__isempty = EmptyStringFilter(field_name='myfield')
class Meta:
model = MyModel
fields = []
Filtering by relative times
---------------------------
Given a model with a timestamp field, it may be useful to filter based on relative times.
For instance, perhaps we want to get data from the past *n* hours.
This could be accomplished the with a ``NumberFilter`` that invokes a custom method.
.. code-block:: python
from django.utils import timezone
from datetime import timedelta
...
class DataModel(models.Model):
time_stamp = models.DateTimeField()
class DataFilter(django_filters.FilterSet):
hours = django_filters.NumberFilter(
field_name='time_stamp', method='get_past_n_hours', label="Past n hours")
def get_past_n_hours(self, queryset, field_name, value):
time_threshold = timezone.now() - timedelta(hours=int(value))
return queryset.filter(time_stamp__gte=time_threshold)
class Meta:
model = DataModel
fields = ('hours',)
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().__init__(data, *args, **kwargs)
Adding model field ``help_text`` to filters
-------------------------------------------
Model field ``help_text`` is not used by filters by default. It can be added
using a simple FilterSet base class::
class HelpfulFilterSet(django_filters.FilterSet):
@classmethod
def filter_for_field(cls, f, name, lookup_expr):
filter = super(HelpfulFilterSet, cls).filter_for_field(f, name, lookup_expr)
filter.extra['help_text'] = f.help_text
return filter