Skip to content

Deprecate DjangoFilter backend #4593

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 20, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions docs/api-guide/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,24 +89,24 @@ Generic filters can also present themselves as HTML controls in the browsable AP

## Setting filter backends

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 = {
'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,
using the `GenericAPIView` class-based views.

import django_filters
from django.contrib.auth.models import User
from myapp.serializers import UserSerializer
from rest_framework import filters
from rest_framework import generics

class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)

## Filtering and object lookups

Expand Down Expand Up @@ -139,12 +139,27 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering

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

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.

pip install django-crispy-forms
Expand Down Expand Up @@ -174,10 +189,9 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
import django_filters
from myapp.models import Product
from myapp.serializers import ProductSerializer
from rest_framework import filters
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')
max_price = django_filters.NumberFilter(name="price", lookup_expr='lte')
class Meta:
Expand All @@ -187,7 +201,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filter_class = ProductFilter


Expand All @@ -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
filters using `Manufacturer` name. For example:

import django_filters
from myapp.models import Product
from myapp.serializers import ProductSerializer
from rest_framework import filters
from rest_framework import generics

class ProductFilter(filters.FilterSet):
class ProductFilter(django_filters.rest_framework.FilterSet):
class Meta:
model = Product
fields = ['category', 'in_stock', 'manufacturer__name']
Expand All @@ -218,10 +232,9 @@ This is nice, but it exposes the Django's double underscore convention as part o
import django_filters
from myapp.models import Product
from myapp.serializers import ProductSerializer
from rest_framework import filters
from rest_framework import generics

class ProductFilter(filters.FilterSet):
class ProductFilter(django_filters.rest_framework.FilterSet):
manufacturer = django_filters.CharFilter(name="manufacturer__name")

class Meta:
Expand Down Expand Up @@ -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-url-filter]: https://github.com/miki725/django-url-filter
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters

2 changes: 1 addition & 1 deletion requirements/requirements-optionals.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Optional packages which may be used with REST framework.
markdown==2.6.4
django-guardian==1.4.6
django-filter==0.14.0
django-filter==0.15.3
coreapi==2.0.8
128 changes: 22 additions & 106 deletions rest_framework/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from __future__ import unicode_literals

import operator
import warnings
from functools import reduce

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.constants import LOOKUP_SEP
Expand All @@ -16,50 +16,10 @@
from django.utils.translation import ugettext_lazy as _

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

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):
"""
Expand All @@ -77,78 +37,34 @@ def get_schema_fields(self, view):
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):
"""
A filter backend that uses django-filter.
"""
default_filter_set = FilterSet
template = filter_template

def __init__(self):
def __new__(cls, *args, **kwargs):
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):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)

if filter_class:
filter_model = filter_class.Meta.model

assert issubclass(queryset.model, filter_model), \
'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
warnings.warn(
"The built in 'rest_framework.filters.DjangoFilterBackend' is pending deprecation. "
"You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.",
PendingDeprecationWarning
)

return AutoFilterSet
from django_filters.rest_framework import DjangoFilterBackend

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 []
return DjangoFilterBackend(*args, **kwargs)


class SearchFilter(BaseFilterBackend):
Expand Down
34 changes: 34 additions & 0 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datetime
import unittest
import warnings
from decimal import Decimal

from django.conf.urls import url
Expand Down Expand Up @@ -134,6 +135,39 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
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')
def test_get_filtered_fields_root_view(self):
"""
Expand Down