Deprecate DjangoFilter backend (#4593)

Deprecate the built-in `rest_framework.filters.DjangoFilterBackend` in favour of the third-party `django_filters.rest_framework.DjangoFilterBackend`.
This commit is contained in:
Tom Christie 2016-10-20 10:47:09 +01:00 committed by GitHub
parent 3f6004c5a9
commit 2395fb5386
4 changed files with 83 additions and 121 deletions

View File

@ -92,21 +92,21 @@ Generic filters can also present themselves as HTML controls in the browsable AP
The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example.
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
} }
You can also set the filter backends on a per-view, or per-viewset basis, You can also set the filter backends on a per-view, or per-viewset basis,
using the `GenericAPIView` class-based views. using the `GenericAPIView` class-based views.
import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from myapp.serializers import UserSerializer from myapp.serializers import UserSerializer
from rest_framework import filters
from rest_framework import generics from rest_framework import generics
class UserListView(generics.ListAPIView): class UserListView(generics.ListAPIView):
queryset = User.objects.all() queryset = User.objects.all()
serializer_class = UserSerializer serializer_class = UserSerializer
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
## Filtering and object lookups ## Filtering and object lookups
@ -139,12 +139,27 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
## DjangoFilterBackend ## DjangoFilterBackend
The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter]. The `django-filter` library includes a `DjangoFilterBackend` class which
supports highly customizable field filtering for REST framework.
To use REST framework's `DjangoFilterBackend`, first install `django-filter`. To use `DjangoFilterBackend`, first install `django-filter`.
pip install django-filter pip install django-filter
You should now either add the filter backend to your settings:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}
Or add the filter backend to an individual View or ViewSet.
from django_filters.rest_framework import DjangoFilterBackend
class UserListView(generics.ListAPIView):
...
filter_backends = (DjangoFilterBackend,)
If you are using the 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. If you are using the 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.
pip install django-crispy-forms pip install django-crispy-forms
@ -174,10 +189,9 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
import django_filters import django_filters
from myapp.models import Product from myapp.models import Product
from myapp.serializers import ProductSerializer from myapp.serializers import ProductSerializer
from rest_framework import filters
from rest_framework import generics from rest_framework import generics
class ProductFilter(filters.FilterSet): class ProductFilter(django_filters.rest_framework.FilterSet):
min_price = django_filters.NumberFilter(name="price", lookup_expr='gte') min_price = django_filters.NumberFilter(name="price", lookup_expr='gte')
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte') max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
class Meta: class Meta:
@ -187,7 +201,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
class ProductList(generics.ListAPIView): class ProductList(generics.ListAPIView):
queryset = Product.objects.all() queryset = Product.objects.all()
serializer_class = ProductSerializer serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filter_class = ProductFilter filter_class = ProductFilter
@ -199,12 +213,12 @@ You can also span relationships using `django-filter`, let's assume that each
product has foreign key to `Manufacturer` model, so we create filter that product has foreign key to `Manufacturer` model, so we create filter that
filters using `Manufacturer` name. For example: filters using `Manufacturer` name. For example:
import django_filters
from myapp.models import Product from myapp.models import Product
from myapp.serializers import ProductSerializer from myapp.serializers import ProductSerializer
from rest_framework import filters
from rest_framework import generics from rest_framework import generics
class ProductFilter(filters.FilterSet): class ProductFilter(django_filters.rest_framework.FilterSet):
class Meta: class Meta:
model = Product model = Product
fields = ['category', 'in_stock', 'manufacturer__name'] fields = ['category', 'in_stock', 'manufacturer__name']
@ -218,10 +232,9 @@ This is nice, but it exposes the Django's double underscore convention as part o
import django_filters import django_filters
from myapp.models import Product from myapp.models import Product
from myapp.serializers import ProductSerializer from myapp.serializers import ProductSerializer
from rest_framework import filters
from rest_framework import generics from rest_framework import generics
class ProductFilter(filters.FilterSet): class ProductFilter(django_filters.rest_framework.FilterSet):
manufacturer = django_filters.CharFilter(name="manufacturer__name") manufacturer = django_filters.CharFilter(name="manufacturer__name")
class Meta: class Meta:
@ -454,4 +467,3 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
[django-url-filter]: https://github.com/miki725/django-url-filter [django-url-filter]: https://github.com/miki725/django-url-filter
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters [drf-url-filter]: https://github.com/manjitkumar/drf-url-filters

View File

@ -1,5 +1,5 @@
# Optional packages which may be used with REST framework. # Optional packages which may be used with REST framework.
markdown==2.6.4 markdown==2.6.4
django-guardian==1.4.6 django-guardian==1.4.6
django-filter==0.14.0 django-filter==0.15.3
coreapi==2.0.8 coreapi==2.0.8

View File

@ -5,9 +5,9 @@ returned by list views.
from __future__ import unicode_literals from __future__ import unicode_literals
import operator import operator
import warnings
from functools import reduce from functools import reduce
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
@ -16,50 +16,10 @@ from django.utils import six
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import ( from rest_framework.compat import (
coreapi, crispy_forms, distinct, django_filters, guardian, template_render coreapi, distinct, django_filters, guardian, template_render
) )
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
if 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms and django_filters:
# If django-crispy-forms is installed, use it to get a bootstrap3 rendering
# of the DjangoFilterBackend controls when displayed as HTML.
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
class FilterSet(django_filters.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None
layout_components = list(self.form.fields.keys()) + [
Submit('', _('Submit'), css_class='btn-default'),
]
helper = FormHelper()
helper.form_method = 'GET'
helper.template_pack = 'bootstrap3'
helper.layout = Layout(*layout_components)
self.form.helper = helper
filter_template = 'rest_framework/filters/django_filter_crispyforms.html'
elif django_filters:
# If django-crispy-forms is not installed, use the standard
# 'form.as_p' rendering when DjangoFilterBackend is displayed as HTML.
class FilterSet(django_filters.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None
filter_template = 'rest_framework/filters/django_filter.html'
else:
FilterSet = None
filter_template = None
class BaseFilterBackend(object): class BaseFilterBackend(object):
""" """
@ -77,78 +37,34 @@ class BaseFilterBackend(object):
return [] return []
class FilterSet(object):
def __new__(cls, *args, **kwargs):
warnings.warn(
"The built in 'rest_framework.filters.FilterSet' is pending deprecation. "
"You should use 'django_filters.rest_framework.FilterSet' instead.",
PendingDeprecationWarning
)
from django_filters.rest_framework import FilterSet
return FilterSet(*args, **kwargs)
class DjangoFilterBackend(BaseFilterBackend): class DjangoFilterBackend(BaseFilterBackend):
""" """
A filter backend that uses django-filter. A filter backend that uses django-filter.
""" """
default_filter_set = FilterSet def __new__(cls, *args, **kwargs):
template = filter_template
def __init__(self):
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
assert django_filters.VERSION >= (0, 15, 3), 'django-filter 0.15.3 and above is required'
def get_filter_class(self, view, queryset=None): warnings.warn(
""" "The built in 'rest_framework.filters.DjangoFilterBackend' is pending deprecation. "
Return the django-filters `FilterSet` used to filter the queryset. "You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.",
""" PendingDeprecationWarning
filter_class = getattr(view, 'filter_class', None) )
filter_fields = getattr(view, 'filter_fields', None)
if filter_class: from django_filters.rest_framework import DjangoFilterBackend
filter_model = filter_class.Meta.model
assert issubclass(queryset.model, filter_model), \ return DjangoFilterBackend(*args, **kwargs)
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
return filter_class
if filter_fields:
class AutoFilterSet(self.default_filter_set):
class Meta:
model = queryset.model
fields = filter_fields
return AutoFilterSet
return None
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset).qs
return queryset
def to_html(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if not filter_class:
return None
filter_instance = filter_class(request.query_params, queryset=queryset)
context = {
'filter': filter_instance
}
template = loader.get_template(self.template)
return template_render(template, context)
def get_schema_fields(self, view):
assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`'
filter_class = getattr(view, 'filter_class', None)
if filter_class:
return [
coreapi.Field(name=field_name, required=False, location='query')
for field_name in filter_class().filters.keys()
]
filter_fields = getattr(view, 'filter_fields', None)
if filter_fields:
return [
coreapi.Field(name=field_name, required=False, location='query')
for field_name in filter_fields
]
return []
class SearchFilter(BaseFilterBackend): class SearchFilter(BaseFilterBackend):

View File

@ -2,6 +2,7 @@ from __future__ import unicode_literals
import datetime import datetime
import unittest import unittest
import warnings
from decimal import Decimal from decimal import Decimal
from django.conf.urls import url from django.conf.urls import url
@ -134,6 +135,39 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
Integration tests for filtered list views. Integration tests for filtered list views.
""" """
@unittest.skipUnless(django_filters, 'django-filter not installed')
def test_backend_deprecation(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
view = FilterFieldsRootView.as_view()
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning))
self.assertIn("'rest_framework.filters.DjangoFilterBackend' is pending deprecation.", str(w[-1].message))
@unittest.skipUnless(django_filters, 'django-filter not installed')
def test_no_df_deprecation(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
import django_filters.rest_framework
class DFFilterFieldsRootView(FilterFieldsRootView):
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
view = DFFilterFieldsRootView.as_view()
request = factory.get('/')
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data)
self.assertEqual(len(w), 0)
@unittest.skipUnless(django_filters, 'django-filter not installed') @unittest.skipUnless(django_filters, 'django-filter not installed')
def test_get_filtered_fields_root_view(self): def test_get_filtered_fields_root_view(self):
""" """