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
|
====================
Integration with DRF
====================
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.
__ http://www.django-rest-framework.org/
__ http://www.django-rest-framework.org/api-guide/filtering/
Quickstart
----------
Using the new ``FilterSet`` simply requires changing the import path. Instead of importing from ``django_filters``, import from the ``rest_framework`` sub-package.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductFilter(filters.FilterSet):
...
Your view class will also need to add ``DjangoFilterBackend`` to the ``filter_backends``.
.. code-block:: python
from django_filters import rest_framework as filters
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('category', 'in_stock')
If you want to use the django-filter backend by default, add it to the ``DEFAULT_FILTER_BACKENDS`` setting.
.. code-block:: python
# settings.py
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
...
),
}
Adding a FilterSet with ``filterset_class``
-------------------------------------------
To enable filtering with a ``FilterSet``, add it to the ``filterset_class`` parameter on your view class.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductFilter(filters.FilterSet):
min_price = filters.NumberFilter(field_name="price", lookup_expr='gte')
max_price = filters.NumberFilter(field_name="price", lookup_expr='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = ProductFilter
Using the ``filterset_fields`` shortcut
---------------------------------------
You may bypass creating a ``FilterSet`` by instead adding ``filterset_fields`` to your view class. This is equivalent to creating a FilterSet with just :ref:`Meta.fields <fields>`.
.. code-block:: python
from rest_framework import generics
from django_filters import rest_framework as filters
from myapp import Product
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('category', 'in_stock')
# Equivalent FilterSet:
class ProductFilter(filters.FilterSet):
class Meta:
model = Product
fields = ('category', 'in_stock')
Note that using ``filterset_fields`` and ``filterset_class`` together is not
supported.
Overriding FilterSet creation
-----------------------------
``FilterSet`` creation can be customized by overriding the following methods on the backend class:
* ``.get_filterset(self, request, queryset, view)``
* ``.get_filterset_class(self, view, queryset=None)``
* ``.get_filterset_kwargs(self, request, queryset, view)``
You can override these methods on a case-by-case basis for each view, creating unique backends, or these methods can be used to write your own hooks to the view class.
.. code-block:: python
class MyFilterBackend(filters.DjangoFilterBackend):
def get_filterset_kwargs(self, request, queryset, view):
kwargs = super().get_filterset_kwargs(request, queryset, view)
# merge filterset kwargs provided by view class
if hasattr(view, 'get_filterset_kwargs'):
kwargs.update(view.get_filterset_kwargs())
return kwargs
class BookFilter(filters.FilterSet):
def __init__(self, *args, author=None, **kwargs):
super().__init__(*args, **kwargs)
# do something w/ author
class BookViewSet(viewsets.ModelViewSet):
filter_backends = [MyFilterBackend]
filterset_class = BookFilter
def get_filterset_kwargs(self):
return {
'author': self.get_author(),
}
Schema Generation with Core API and Open API
--------------------------------------------
The backend class integrates with DRF's schema generation by implementing ``get_schema_fields()`` and ``get_schema_operation_parameters()``. ``get_schema_fields()`` is automatically enabled when Core API is installed. ``get_schema_operation_parameters()`` is always enabled for Open API (new since DRF 3.9). Schema generation usually functions seamlessly, however the implementation does expect to invoke the view's ``get_queryset()`` method. There is a caveat in that views are artificially constructed during schema generation, so the ``args`` and ``kwargs`` attributes will be empty. If you depend on arguments parsed from the URL, you will need to handle their absence in ``get_queryset()``.
For example, your get queryset method may look like this:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_project(self):
return models.Project.objects.get(pk=self.kwargs['project_id'])
def get_queryset(self):
project = self.get_project()
return self.queryset \
.filter(project=project) \
.filter(author=self.request.user)
This could be rewritten like so:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_project(self):
try:
return models.Project.objects.get(pk=self.kwargs['project_id'])
except models.Project.DoesNotExist:
return None
def get_queryset(self):
project = self.get_project()
if project is None:
return self.queryset.none()
return self.queryset \
.filter(project=project) \
.filter(author=self.request.user)
Or more simply as:
.. code-block:: python
class IssueViewSet(views.ModelViewSet):
queryset = models.Issue.objects.all()
def get_queryset(self):
# project_id may be None
return self.queryset \
.filter(project_id=self.kwargs.get('project_id')) \
.filter(author=self.request.user)
Crispy Forms
------------
If you are using DRF's browsable API or admin API you may also want to install ``django-crispy-forms``, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. Note that this isn't actively supported, although pull requests for bug fixes are welcome.
.. code-block:: bash
pip install django-crispy-forms
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.
|