Repository: carltongibson/django-filter

Issue:
Rest Framework filterset & django.forms.ValidationError
When using Django Rest Framework filterset (*django_filters.rest_framework.FilterSet*) with "strict = 'RAISE'" meta option, *django-filter* raises usual ValidationError from Django instead of DRF ValidationError. This has the effect that default DRF exception handler does not catch this exception, and it results in a 500 response.

```python
class KittenFilter(filters.FilterSet):
    class Meta:
        fields = ('color', 'name', 'weight')
        model = Kitten
        strict = 'RAISE'
```



Discussion:
Yeah it does. (That's because the exception is raised during the FilterSet's form validation.)

If you set `strict='RAISE'`you need to be prepared to handle the exception. [See here in the DRF docs for guidance](http://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling). 
Thanks for quick response!  Once there's a Rest Framework-specific filterset class, isn't that intuitive to expect it to conform to Rest Framework conventions? In principle, it is possible to do smth like that:
```python
class BaseFilterSet(object):
   ...
   validation_error_cls = django.exc.ValidationError
   ...
   @property
   def qs(self):
      ...
      raise self.validation_error_cls(self.form.errors)
      ...

# In a rest_framework.filterset
class FilterSet(filterset.FilterSet):
    validation_error_cls = rest_framework.exc.ValidationError
```

What do you think about such a change?
I'm happy for users to define such a class in their own projects. 

I'm not sure it should be the default. 

This kind of thing always comes up on DRF. "Handle all exceptions automatically". It's always rejected there. "We handle the common cases, it's up to the user to deal with the project specific edge cases". I tend to agree. 

I'd certainly take an example with discussion added to the Tips and Tricks section of the docs. 
OK, thanks for your comments!

Resolving patch:
diff --git a/django_filters/rest_framework/filterset.py b/django_filters/rest_framework/filterset.py
--- a/django_filters/rest_framework/filterset.py
+++ b/django_filters/rest_framework/filterset.py
@@ -3,11 +3,12 @@
 from copy import deepcopy
 
 from django.db import models
+from django import forms
 from django.utils.translation import ugettext_lazy as _
 
 from django_filters import filterset
 from .filters import BooleanFilter, IsoDateTimeFilter
-from .. import compat
+from .. import compat, utils
 
 
 FILTER_FOR_DBFIELD_DEFAULTS = deepcopy(filterset.FILTER_FOR_DBFIELD_DEFAULTS)
@@ -36,3 +37,12 @@ def __init__(self, *args, **kwargs):
             helper.layout = Layout(*layout_components)
 
             self.form.helper = helper
+
+    @property
+    def qs(self):
+        from rest_framework.exceptions import ValidationError
+
+        try:
+            return super(FilterSet, self).qs
+        except forms.ValidationError as e:
+            raise ValidationError(utils.raw_validation(e))
diff --git a/django_filters/utils.py b/django_filters/utils.py
--- a/django_filters/utils.py
+++ b/django_filters/utils.py
@@ -8,6 +8,7 @@
 from django.db.models.expressions import Expression
 from django.db.models.fields import FieldDoesNotExist
 from django.db.models.fields.related import RelatedField, ForeignObjectRel
+from django.forms import ValidationError
 from django.utils import six, timezone
 from django.utils.encoding import force_text
 from django.utils.translation import ugettext as _
@@ -232,3 +233,24 @@ def label_for_filter(model, field_name, lookup_expr, exclude=False):
     verbose_expression = pretty_name(' '.join(verbose_expression))
 
     return verbose_expression
+
+
+def raw_validation(error):
+    """
+    Deconstruct a django.forms.ValidationError into a primitive structure
+    eg, plain dicts and lists.
+    """
+    if isinstance(error, ValidationError):
+        if hasattr(error, 'error_dict'):
+            error = error.error_dict
+        elif not hasattr(error, 'message'):
+            error = error.error_list
+        else:
+            error = error.message
+
+    if isinstance(error, dict):
+        return {key: raw_validation(value) for key, value in error.items()}
+    elif isinstance(error, list):
+        return [raw_validation(value) for value in error]
+    else:
+        return error


Test patch:
diff --git a/tests/rest_framework/test_backends.py b/tests/rest_framework/test_backends.py
--- a/tests/rest_framework/test_backends.py
+++ b/tests/rest_framework/test_backends.py
@@ -1,29 +1,19 @@
 from __future__ import unicode_literals
 
-import datetime
-from decimal import Decimal
 from unittest import skipIf
 
-from django.conf.urls import url
 from django.db.models import BooleanField
 from django.test import TestCase
 from django.test.utils import override_settings
-from django.utils.dateparse import parse_date
 
-try:
-    from django.urls import reverse
-except ImportError:
-    # Django < 1.10 compatibility
-    from django.core.urlresolvers import reverse
-
-from rest_framework import generics, serializers, status
+from rest_framework import generics, serializers
 from rest_framework.test import APIRequestFactory
 
 from django_filters import compat, filters
 from django_filters.rest_framework import DjangoFilterBackend, FilterSet
 from django_filters.rest_framework import backends
 
-from .models import BaseFilterableItem, BasicModel, FilterableItem, DjangoFilterOrderingModel
+from .models import FilterableItem
 
 factory = APIRequestFactory()
 
@@ -60,69 +50,6 @@ class FilterClassRootView(generics.ListCreateAPIView):
     filter_backends = (DjangoFilterBackend,)
 
 
-# These classes are used to test a misconfigured filter class.
-class MisconfiguredFilter(FilterSet):
-    text = filters.CharFilter(lookup_expr='icontains')
-
-    class Meta:
-        model = BasicModel
-        fields = ['text']
-
-
-class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
-    queryset = FilterableItem.objects.all()
-    serializer_class = FilterableItemSerializer
-    filter_class = MisconfiguredFilter
-    filter_backends = (DjangoFilterBackend,)
-
-
-class FilterClassDetailView(generics.RetrieveAPIView):
-    queryset = FilterableItem.objects.all()
-    serializer_class = FilterableItemSerializer
-    filter_class = SeveralFieldsFilter
-    filter_backends = (DjangoFilterBackend,)
-
-
-# These classes are used to test base model filter support
-class BaseFilterableItemFilter(FilterSet):
-    text = filters.CharFilter()
-
-    class Meta:
-        model = BaseFilterableItem
-        fields = '__all__'
-
-
-class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
-    queryset = FilterableItem.objects.all()
-    serializer_class = FilterableItemSerializer
-    filter_class = BaseFilterableItemFilter
-    filter_backends = (DjangoFilterBackend,)
-
-
-# Regression test for #814
-class FilterFieldsQuerysetView(generics.ListCreateAPIView):
-    queryset = FilterableItem.objects.all()
-    serializer_class = FilterableItemSerializer
-    filter_fields = ['decimal', 'date']
-    filter_backends = (DjangoFilterBackend,)
-
-
-class GetQuerysetView(generics.ListCreateAPIView):
-    serializer_class = FilterableItemSerializer
-    filter_class = SeveralFieldsFilter
-    filter_backends = (DjangoFilterBackend,)
-
-    def get_queryset(self):
-        return FilterableItem.objects.all()
-
-
-urlpatterns = [
-    url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
-    url(r'^$', FilterClassRootView.as_view(), name='root-view'),
-    url(r'^get-queryset/$', GetQuerysetView.as_view(), name='get-queryset-view'),
-]
-
-
 @skipIf(compat.coreapi is None, 'coreapi must be installed')
 class GetSchemaFieldsTests(TestCase):
     def test_fields_with_filter_fields_list(self):
@@ -165,176 +92,7 @@ def test_fields_with_filter_class(self):
         self.assertEqual(fields, ['text', 'decimal', 'date'])
 
 
-class CommonFilteringTestCase(TestCase):
-    def _serialize_object(self, obj):
-        return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
-
-    def setUp(self):
-        """
-        Create 10 FilterableItem instances.
-        """
-        base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
-        for i in range(10):
-            text = chr(i + ord(base_data[0])) * 3  # Produces string 'aaa', 'bbb', etc.
-            decimal = base_data[1] + i
-            date = base_data[2] - datetime.timedelta(days=i * 2)
-            FilterableItem(text=text, decimal=decimal, date=date).save()
-
-        self.objects = FilterableItem.objects
-        self.data = [
-            self._serialize_object(obj)
-            for obj in self.objects.all()
-        ]
-
-
-class IntegrationTestFiltering(CommonFilteringTestCase):
-    """
-    Integration tests for filtered list views.
-    """
-
-    def test_get_filtered_fields_root_view(self):
-        """
-        GET requests to paginated ListCreateAPIView should return paginated results.
-        """
-        view = FilterFieldsRootView.as_view()
-
-        # Basic test with no filter.
-        request = factory.get('/')
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, self.data)
-
-        # Tests that the decimal filter works.
-        search_decimal = Decimal('2.25')
-        request = factory.get('/', {'decimal': '%s' % search_decimal})
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
-        self.assertEqual(response.data, expected_data)
-
-        # Tests that the date filter works.
-        search_date = datetime.date(2012, 9, 22)
-        request = factory.get('/', {'date': '%s' % search_date})  # search_date str: '2012-09-22'
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
-        self.assertEqual(response.data, expected_data)
-
-    def test_filter_with_queryset(self):
-        """
-        Regression test for #814.
-        """
-        view = FilterFieldsQuerysetView.as_view()
-
-        # Tests that the decimal filter works.
-        search_decimal = Decimal('2.25')
-        request = factory.get('/', {'decimal': '%s' % search_decimal})
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
-        self.assertEqual(response.data, expected_data)
-
-    def test_filter_with_get_queryset_only(self):
-        """
-        Regression test for #834.
-        """
-        view = GetQuerysetView.as_view()
-        request = factory.get('/get-queryset/')
-        view(request).render()
-        # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
-        # here when neither `model' nor `queryset' was specified.
-
-    def test_get_filtered_class_root_view(self):
-        """
-        GET requests to filtered ListCreateAPIView that have a filter_class set
-        should return filtered results.
-        """
-        view = FilterClassRootView.as_view()
-
-        # Basic test with no filter.
-        request = factory.get('/')
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, self.data)
-
-        # Tests that the decimal filter set with 'lt' in the filter class works.
-        search_decimal = Decimal('4.25')
-        request = factory.get('/', {'decimal': '%s' % search_decimal})
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
-        self.assertEqual(response.data, expected_data)
-
-        # Tests that the date filter set with 'gt' in the filter class works.
-        search_date = datetime.date(2012, 10, 2)
-        request = factory.get('/', {'date': '%s' % search_date})  # search_date str: '2012-10-02'
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
-        self.assertEqual(response.data, expected_data)
-
-        # Tests that the text filter set with 'icontains' in the filter class works.
-        search_text = 'ff'
-        request = factory.get('/', {'text': '%s' % search_text})
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if search_text in f['text'].lower()]
-        self.assertEqual(response.data, expected_data)
-
-        # Tests that multiple filters works.
-        search_decimal = Decimal('5.25')
-        search_date = datetime.date(2012, 10, 2)
-        request = factory.get('/', {
-            'decimal': '%s' % (search_decimal,),
-            'date': '%s' % (search_date,)
-        })
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
-                         Decimal(f['decimal']) < search_decimal]
-        self.assertEqual(response.data, expected_data)
-
-    def test_incorrectly_configured_filter(self):
-        """
-        An error should be displayed when the filter class is misconfigured.
-        """
-        view = IncorrectlyConfiguredRootView.as_view()
-
-        request = factory.get('/')
-        self.assertRaises(AssertionError, view, request)
-
-    def test_base_model_filter(self):
-        """
-        The `get_filter_class` model checks should allow base model filters.
-        """
-        view = BaseFilterableItemFilterRootView.as_view()
-
-        request = factory.get('/?text=aaa')
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(len(response.data), 1)
-
-    def test_unknown_filter(self):
-        """
-        GET requests with filters that aren't configured should return 200.
-        """
-        view = FilterFieldsRootView.as_view()
-
-        search_integer = 10
-        request = factory.get('/', {'integer': '%s' % search_integer})
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-    def test_html_rendering(self):
-        """
-        Make sure response renders w/ backend
-        """
-        view = FilterFieldsRootView.as_view()
-        request = factory.get('/')
-        request.META['HTTP_ACCEPT'] = 'text/html'
-        response = view(request).render()
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-
+class TemplateTests(TestCase):
     def test_backend_output(self):
         """
         Ensure backend renders default if template path does not exist
@@ -391,103 +149,6 @@ def test_multiple_engines(self):
             self.test_backend_output()
 
 
-@override_settings(ROOT_URLCONF='tests.rest_framework.test_backends')
-class IntegrationTestDetailFiltering(CommonFilteringTestCase):
-    """
-    Integration tests for filtered detail views.
-    """
-    def _get_url(self, item):
-        return reverse('detail-view', kwargs=dict(pk=item.pk))
-
-    def test_get_filtered_detail_view(self):
-        """
-        GET requests to filtered RetrieveAPIView that have a filter_class set
-        should return filtered results.
-        """
-        item = self.objects.all()[0]
-        data = self._serialize_object(item)
-
-        # Basic test with no filter.
-        response = self.client.get(self._get_url(item))
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, data)
-
-        # Tests that the decimal filter set that should fail.
-        search_decimal = Decimal('4.25')
-        high_item = self.objects.filter(decimal__gt=search_decimal)[0]
-        response = self.client.get(
-            '{url}'.format(url=self._get_url(high_item)),
-            {'decimal': '{param}'.format(param=search_decimal)})
-        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
-        # Tests that the decimal filter set that should succeed.
-        search_decimal = Decimal('4.25')
-        low_item = self.objects.filter(decimal__lt=search_decimal)[0]
-        low_item_data = self._serialize_object(low_item)
-        response = self.client.get(
-            '{url}'.format(url=self._get_url(low_item)),
-            {'decimal': '{param}'.format(param=search_decimal)})
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, low_item_data)
-
-        # Tests that multiple filters works.
-        search_decimal = Decimal('5.25')
-        search_date = datetime.date(2012, 10, 2)
-        valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
-        valid_item_data = self._serialize_object(valid_item)
-        response = self.client.get(
-            '{url}'.format(url=self._get_url(valid_item)), {
-                'decimal': '{decimal}'.format(decimal=search_decimal),
-                'date': '{date}'.format(date=search_date)
-            })
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-        self.assertEqual(response.data, valid_item_data)
-
-
-class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = DjangoFilterOrderingModel
-        fields = '__all__'
-
-
-class DjangoFilterOrderingTests(TestCase):
-    def setUp(self):
-        data = [{
-            'date': datetime.date(2012, 10, 8),
-            'text': 'abc'
-        }, {
-            'date': datetime.date(2013, 10, 8),
-            'text': 'bcd'
-        }, {
-            'date': datetime.date(2014, 10, 8),
-            'text': 'cde'
-        }]
-
-        for d in data:
-            DjangoFilterOrderingModel.objects.create(**d)
-
-    def test_default_ordering(self):
-        class DjangoFilterOrderingView(generics.ListAPIView):
-            serializer_class = DjangoFilterOrderingSerializer
-            queryset = DjangoFilterOrderingModel.objects.all()
-            filter_backends = (DjangoFilterBackend,)
-            filter_fields = ['text']
-            ordering = ('-date',)
-
-        view = DjangoFilterOrderingView.as_view()
-        request = factory.get('/')
-        response = view(request)
-
-        self.assertEqual(
-            response.data,
-            [
-                {'id': 3, 'date': '2014-10-08', 'text': 'cde'},
-                {'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
-                {'id': 1, 'date': '2012-10-08', 'text': 'abc'}
-            ]
-        )
-
-
 class DefaultFilterSetTests(TestCase):
     def test_default_meta_inheritance(self):
         # https://github.com/carltongibson/django-filter/issues/663
diff --git a/tests/rest_framework/test_integration.py b/tests/rest_framework/test_integration.py
new file mode 100644
--- /dev/null
+++ b/tests/rest_framework/test_integration.py
@@ -0,0 +1,401 @@
+from __future__ import unicode_literals
+
+import datetime
+from decimal import Decimal
+
+from django.conf.urls import url
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.utils.dateparse import parse_date
+
+try:
+    from django.urls import reverse
+except ImportError:
+    # Django < 1.10 compatibility
+    from django.core.urlresolvers import reverse
+
+from rest_framework import generics, serializers, status
+from rest_framework.test import APIRequestFactory
+
+from django_filters import filters, STRICTNESS
+from django_filters.rest_framework import DjangoFilterBackend, FilterSet
+
+from .models import BaseFilterableItem, BasicModel, FilterableItem, DjangoFilterOrderingModel
+
+factory = APIRequestFactory()
+
+
+class FilterableItemSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = FilterableItem
+        fields = '__all__'
+
+
+# Basic filter on a list view.
+class FilterFieldsRootView(generics.ListCreateAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_fields = ['decimal', 'date']
+    filter_backends = (DjangoFilterBackend,)
+
+
+# These class are used to test a filter class.
+class SeveralFieldsFilter(FilterSet):
+    text = filters.CharFilter(lookup_expr='icontains')
+    decimal = filters.NumberFilter(lookup_expr='lt')
+    date = filters.DateFilter(lookup_expr='gt')
+
+    class Meta:
+        model = FilterableItem
+        fields = ['text', 'decimal', 'date']
+
+
+class FilterClassRootView(generics.ListCreateAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_class = SeveralFieldsFilter
+    filter_backends = (DjangoFilterBackend,)
+
+
+# These classes are used to test a misconfigured filter class.
+class MisconfiguredFilter(FilterSet):
+    text = filters.CharFilter(lookup_expr='icontains')
+
+    class Meta:
+        model = BasicModel
+        fields = ['text']
+
+
+class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_class = MisconfiguredFilter
+    filter_backends = (DjangoFilterBackend,)
+
+
+class FilterClassDetailView(generics.RetrieveAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_class = SeveralFieldsFilter
+    filter_backends = (DjangoFilterBackend,)
+
+
+# These classes are used to test base model filter support
+class BaseFilterableItemFilter(FilterSet):
+    text = filters.CharFilter()
+
+    class Meta:
+        model = BaseFilterableItem
+        fields = '__all__'
+
+
+class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_class = BaseFilterableItemFilter
+    filter_backends = (DjangoFilterBackend,)
+
+
+# Regression test for #814
+class FilterFieldsQuerysetView(generics.ListCreateAPIView):
+    queryset = FilterableItem.objects.all()
+    serializer_class = FilterableItemSerializer
+    filter_fields = ['decimal', 'date']
+    filter_backends = (DjangoFilterBackend,)
+
+
+class GetQuerysetView(generics.ListCreateAPIView):
+    serializer_class = FilterableItemSerializer
+    filter_class = SeveralFieldsFilter
+    filter_backends = (DjangoFilterBackend,)
+
+    def get_queryset(self):
+        return FilterableItem.objects.all()
+
+
+urlpatterns = [
+    url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
+    url(r'^$', FilterClassRootView.as_view(), name='root-view'),
+    url(r'^get-queryset/$', GetQuerysetView.as_view(), name='get-queryset-view'),
+]
+
+
+class CommonFilteringTestCase(TestCase):
+    def _serialize_object(self, obj):
+        return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
+
+    def setUp(self):
+        """
+        Create 10 FilterableItem instances.
+        """
+        base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
+        for i in range(10):
+            text = chr(i + ord(base_data[0])) * 3  # Produces string 'aaa', 'bbb', etc.
+            decimal = base_data[1] + i
+            date = base_data[2] - datetime.timedelta(days=i * 2)
+            FilterableItem(text=text, decimal=decimal, date=date).save()
+
+        self.objects = FilterableItem.objects
+        self.data = [
+            self._serialize_object(obj)
+            for obj in self.objects.all()
+        ]
+
+
+class IntegrationTestFiltering(CommonFilteringTestCase):
+    """
+    Integration tests for filtered list views.
+    """
+
+    def test_get_filtered_fields_root_view(self):
+        """
+        GET requests to paginated ListCreateAPIView should return paginated results.
+        """
+        view = FilterFieldsRootView.as_view()
+
+        # Basic test with no filter.
+        request = factory.get('/')
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data, self.data)
+
+        # Tests that the decimal filter works.
+        search_decimal = Decimal('2.25')
+        request = factory.get('/', {'decimal': '%s' % search_decimal})
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+        self.assertEqual(response.data, expected_data)
+
+        # Tests that the date filter works.
+        search_date = datetime.date(2012, 9, 22)
+        request = factory.get('/', {'date': '%s' % search_date})  # search_date str: '2012-09-22'
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
+        self.assertEqual(response.data, expected_data)
+
+    def test_filter_with_queryset(self):
+        """
+        Regression test for #814.
+        """
+        view = FilterFieldsQuerysetView.as_view()
+
+        # Tests that the decimal filter works.
+        search_decimal = Decimal('2.25')
+        request = factory.get('/', {'decimal': '%s' % search_decimal})
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+        self.assertEqual(response.data, expected_data)
+
+    def test_filter_with_get_queryset_only(self):
+        """
+        Regression test for #834.
+        """
+        view = GetQuerysetView.as_view()
+        request = factory.get('/get-queryset/')
+        view(request).render()
+        # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
+        # here when neither `model' nor `queryset' was specified.
+
+    def test_get_filtered_class_root_view(self):
+        """
+        GET requests to filtered ListCreateAPIView that have a filter_class set
+        should return filtered results.
+        """
+        view = FilterClassRootView.as_view()
+
+        # Basic test with no filter.
+        request = factory.get('/')
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data, self.data)
+
+        # Tests that the decimal filter set with 'lt' in the filter class works.
+        search_decimal = Decimal('4.25')
+        request = factory.get('/', {'decimal': '%s' % search_decimal})
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
+        self.assertEqual(response.data, expected_data)
+
+        # Tests that the date filter set with 'gt' in the filter class works.
+        search_date = datetime.date(2012, 10, 2)
+        request = factory.get('/', {'date': '%s' % search_date})  # search_date str: '2012-10-02'
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
+        self.assertEqual(response.data, expected_data)
+
+        # Tests that the text filter set with 'icontains' in the filter class works.
+        search_text = 'ff'
+        request = factory.get('/', {'text': '%s' % search_text})
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if search_text in f['text'].lower()]
+        self.assertEqual(response.data, expected_data)
+
+        # Tests that multiple filters works.
+        search_decimal = Decimal('5.25')
+        search_date = datetime.date(2012, 10, 2)
+        request = factory.get('/', {
+            'decimal': '%s' % (search_decimal,),
+            'date': '%s' % (search_date,)
+        })
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
+                         Decimal(f['decimal']) < search_decimal]
+        self.assertEqual(response.data, expected_data)
+
+    def test_incorrectly_configured_filter(self):
+        """
+        An error should be displayed when the filter class is misconfigured.
+        """
+        view = IncorrectlyConfiguredRootView.as_view()
+
+        request = factory.get('/')
+        self.assertRaises(AssertionError, view, request)
+
+    def test_base_model_filter(self):
+        """
+        The `get_filter_class` model checks should allow base model filters.
+        """
+        view = BaseFilterableItemFilterRootView.as_view()
+
+        request = factory.get('/?text=aaa')
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(len(response.data), 1)
+
+    def test_unknown_filter(self):
+        """
+        GET requests with filters that aren't configured should return 200.
+        """
+        view = FilterFieldsRootView.as_view()
+
+        search_integer = 10
+        request = factory.get('/', {'integer': '%s' % search_integer})
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    def test_html_rendering(self):
+        """
+        Make sure response renders w/ backend
+        """
+        view = FilterFieldsRootView.as_view()
+        request = factory.get('/')
+        request.META['HTTP_ACCEPT'] = 'text/html'
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+    @override_settings(FILTERS_STRICTNESS=STRICTNESS.RAISE_VALIDATION_ERROR)
+    def test_strictness_validation_error(self):
+        """
+        Ensure validation errors return a proper error response instead of
+        an internal server error.
+        """
+        view = FilterFieldsRootView.as_view()
+        request = factory.get('/?decimal=foobar')
+        response = view(request).render()
+        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+        self.assertEqual(response.data, {'decimal': ['Enter a number.']})
+
+
+@override_settings(ROOT_URLCONF='tests.rest_framework.test_integration')
+class IntegrationTestDetailFiltering(CommonFilteringTestCase):
+    """
+    Integration tests for filtered detail views.
+    """
+
+    def _get_url(self, item):
+        return reverse('detail-view', kwargs=dict(pk=item.pk))
+
+    def test_get_filtered_detail_view(self):
+        """
+        GET requests to filtered RetrieveAPIView that have a filter_class set
+        should return filtered results.
+        """
+        item = self.objects.all()[0]
+        data = self._serialize_object(item)
+
+        # Basic test with no filter.
+        response = self.client.get(self._get_url(item))
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data, data)
+
+        # Tests that the decimal filter set that should fail.
+        search_decimal = Decimal('4.25')
+        high_item = self.objects.filter(decimal__gt=search_decimal)[0]
+        response = self.client.get(
+            '{url}'.format(url=self._get_url(high_item)),
+            {'decimal': '{param}'.format(param=search_decimal)})
+        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+        # Tests that the decimal filter set that should succeed.
+        search_decimal = Decimal('4.25')
+        low_item = self.objects.filter(decimal__lt=search_decimal)[0]
+        low_item_data = self._serialize_object(low_item)
+        response = self.client.get(
+            '{url}'.format(url=self._get_url(low_item)),
+            {'decimal': '{param}'.format(param=search_decimal)})
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data, low_item_data)
+
+        # Tests that multiple filters works.
+        search_decimal = Decimal('5.25')
+        search_date = datetime.date(2012, 10, 2)
+        valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
+        valid_item_data = self._serialize_object(valid_item)
+        response = self.client.get(
+            '{url}'.format(url=self._get_url(valid_item)), {
+                'decimal': '{decimal}'.format(decimal=search_decimal),
+                'date': '{date}'.format(date=search_date)
+            })
+        self.assertEqual(response.status_code, status.HTTP_200_OK)
+        self.assertEqual(response.data, valid_item_data)
+
+
+class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = DjangoFilterOrderingModel
+        fields = '__all__'
+
+
+class DjangoFilterOrderingTests(TestCase):
+    def setUp(self):
+        data = [{
+            'date': datetime.date(2012, 10, 8),
+            'text': 'abc'
+        }, {
+            'date': datetime.date(2013, 10, 8),
+            'text': 'bcd'
+        }, {
+            'date': datetime.date(2014, 10, 8),
+            'text': 'cde'
+        }]
+
+        for d in data:
+            DjangoFilterOrderingModel.objects.create(**d)
+
+    def test_default_ordering(self):
+        class DjangoFilterOrderingView(generics.ListAPIView):
+            serializer_class = DjangoFilterOrderingSerializer
+            queryset = DjangoFilterOrderingModel.objects.all()
+            filter_backends = (DjangoFilterBackend,)
+            filter_fields = ['text']
+            ordering = ('-date',)
+
+        view = DjangoFilterOrderingView.as_view()
+        request = factory.get('/')
+        response = view(request)
+
+        self.assertEqual(
+            response.data,
+            [
+                {'id': 3, 'date': '2014-10-08', 'text': 'cde'},
+                {'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
+                {'id': 1, 'date': '2012-10-08', 'text': 'abc'}
+            ]
+        )
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -5,15 +5,17 @@
 from django.db import models
 from django.db.models.constants import LOOKUP_SEP
 from django.db.models.fields.related import ForeignObjectRel
+from django.forms import ValidationError
 from django.test import TestCase, override_settings
 from django.utils.functional import Promise
 from django.utils.timezone import get_default_timezone
 
+from django_filters import FilterSet, STRICTNESS
+from django_filters.exceptions import FieldLookupError
 from django_filters.utils import (
-    get_field_parts, get_model_field, resolve_field,
-    verbose_field_name, verbose_lookup_expr, label_for_filter, handle_timezone
+    get_field_parts, get_model_field, resolve_field, handle_timezone,
+    verbose_field_name, verbose_lookup_expr, label_for_filter, raw_validation,
 )
-from django_filters.exceptions import FieldLookupError
 
 from .models import User
 from .models import Article
@@ -303,3 +305,21 @@ def test_handle_dst_starting(self):
         dst_starting_date = datetime.datetime(2017, 10, 15, 0, 0, 0, 0)
         handled = handle_timezone(dst_starting_date, True)
         self.assertEqual(handled, get_default_timezone().localize(dst_starting_date, True))
+
+
+class RawValidationDataTests(TestCase):
+    def test_simple(self):
+        class F(FilterSet):
+            class Meta:
+                model = Article
+                fields = ['id', 'author', 'name']
+                strict = STRICTNESS.RAISE_VALIDATION_ERROR
+
+        f = F(data={'id': 'foo', 'author': 'bar', 'name': 'baz'})
+        with self.assertRaises(ValidationError) as exc:
+            f.qs
+
+        self.assertDictEqual(raw_validation(exc.exception), {
+            'id': ['Enter a number.'],
+            'author': ['Select a valid choice. That choice is not one of the available choices.'],
+        })