|
| 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