Repository: encode/django-rest-framework

Issue:
Extract Django-Filter related code
As per discussion on #3315, it would be nice to pull the Django Filter related code out of DRF. 

I'm happy to pull this into Django Filter itself. There's already an [issue there to add a more DRF friendly base FilterSet class](https://github.com/alex/django-filter/issues/275). It makes sense to combine the work into one. 

Moving the code is simple enough — it should require a no more than a change of import for end users. Other queries: test coverage boundaries; docs. 



Discussion:
We'll also need to follow the deprecation path on DRF about this.

@xordoquy — Ah, yes. Good point. 

In the past for cases where we can simply say "'x' is now an external package, make sure to install it." we've gone ahead and moved out of core without any further work. We could either:
- Just force it as an external package.
- Keep eg. `rest_framework.filters.DjangoFilterBackend` as a synonym during the deprecation process. (but still force the external install, rather than keeping the code locally)
- Keep the code local (duplicated) during the deprecation process.

Since `DjangoFilterBackend` relies on Django-Filter (already) there's no harm removing the code — leaving the alias, as you say. 

Don't forget to use @jpadilla's excellent cookiecutter template when you take this on. :)

http://www.django-rest-framework.org/topics/third-party-resources/#how-to-create-a-third-party-package

I've still got an open task to remove the potential 'crispy-forms' dependency for the "filters in the browsable API" not sure best way to tackle that.

> ... excellent cookiecutter template ...

Just for my sanity, I'm thinking of putting this straight into Django Filter. 

```
from django_filters.rest_framework import FilterBackend
```

... or such

> ... the potential 'crispy-forms' dependency ...

Yeah. I saw that. I'm in two-minds there. 

At what point do we say, "Here's the hook and a simple default implementation — anything else you need is up to you"? i.e. Why not keep the dependency there — for the sake of working on (cough) _better things_? 

> Just for my sanity, I'm thinking of putting this straight into Django Filter.

Great plan!

> Why not keep the dependency there — for the sake of working on (cough) better things?

Think I'm happy either way - with crispy or without.

I'm tempted by the 3rd option - Keep the code local (duplicated) during the deprecation process.
I really believe DRF will benefit having a strong deprecation policy en particular due to its popularity and a relatively fast release cycle.

@carltongibson BTW, the link to the issue in this description is broken (404 here).

Yeah. OK fine. (Although the API and behaviour would remain unchanged even if we move the code — and it's the API and behaviour, not the implementation, that's subject to the deprecation policy right?)

I will work first on the DF side of it (this week). I'll open a PR moving the Filter Backend into DF. 

Once the target end is in place I'll come back and we can settle how to handle it the DRF end. 

(@xordoquy Ta. Fixed.)

> it's the API and behaviour, not the implementation, that's subject to the deprecation policy right?

That's correct, but you may change a lot by requiring an update to an extra dependency in particular the valid Django version and co.

Sure. Lets review when we come to it. 

Quick update: This is on my radar for over the holiday period. If that's not quick enough, PRs on Django Filter welcome :-)

> I really believe DRF will benefit having a strong deprecation policy en particular due to its popularity and a relatively fast release cycle.

I think I agree here. Everyone I speak to who's using Django is using DRF. 

@tomchristie If you want something Django-Filter related. You could take this on. I'm happy to have this and maintain it in a `django_filters.rest_framework` module of the main Django-Filter repo — I just haven't got there. 

Equally, there's no urgency to move it, so no rush. (Some of the other things seem more exciting.)

I think this would be valid, yup.
Probably not top of my list, at the moment, tho _possible_ that it'd be worth doing for the 3.4.0 release.

Right. Only about a year late but, progress. 

Off the back of excellent effort from @rpkilby, we'll have the migrated `DjangoFilterBackend` code in Django Filter via https://github.com/carltongibson/django-filter/issues/476

This will go in this week. It'll be on PyPI shortly afterwards. 

What will be missing there is decent docs: the PR has been sat for _ages™_ waiting and I'd rather merge it and add them after than let the updated PR get stale with little conflicts again. (There are only a couple more points until DF hits Inbox Zero, and it's even going to have a 1.0 at last; the docs is part of that, so I'm happy they'll actually happen, at some point 🙂 )

I'm pinging to restart discussion on a roadmap and tasks — including documentation — for dropping the code from DRF. 

Very nice 👍😎


Resolving patch:
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -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
@@ -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):
     """
@@ -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):


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -2,6 +2,7 @@
 
 import datetime
 import unittest
+import warnings
 from decimal import Decimal
 
 from django.conf.urls import url
@@ -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):
         """