diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 168bccf83..528340d69 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -182,13 +182,6 @@ except ImportError: coreschema = None -# django-filter is optional -try: - import django_filters -except ImportError: - django_filters = None - - # django-crispy-forms is optional try: import crispy_forms diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 63ebf05ef..0473787bb 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -5,7 +5,6 @@ returned by list views. from __future__ import unicode_literals import operator -import warnings from functools import reduce from django.core.exceptions import ImproperlyConfigured @@ -18,7 +17,7 @@ from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import ( - coreapi, coreschema, distinct, django_filters, guardian, template_render + coreapi, coreschema, distinct, guardian, template_render ) from rest_framework.settings import api_settings @@ -40,44 +39,6 @@ class BaseFilterBackend(object): return [] -if django_filters: - from django_filters.rest_framework.filterset import FilterSet as DFFilterSet - - class FilterSet(DFFilterSet): - def __init__(self, *args, **kwargs): - warnings.warn( - "The built in 'rest_framework.filters.FilterSet' is deprecated. " - "You should use 'django_filters.rest_framework.FilterSet' instead.", - DeprecationWarning, stacklevel=2 - ) - return super(FilterSet, self).__init__(*args, **kwargs) - - DFBase = django_filters.rest_framework.DjangoFilterBackend - -else: - def FilterSet(): - assert False, 'django-filter must be installed to use the `FilterSet` class' - - DFBase = BaseFilterBackend - - -class DjangoFilterBackend(DFBase): - """ - A filter backend that uses django-filter. - """ - 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' - - warnings.warn( - "The built in 'rest_framework.filters.DjangoFilterBackend' is deprecated. " - "You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.", - DeprecationWarning, stacklevel=2 - ) - - return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs) - - class SearchFilter(BaseFilterBackend): # The URL query parameter used for the search. search_param = api_settings.SEARCH_PARAM diff --git a/tests/models.py b/tests/models.py index 6c9dde8fa..e5d49a0a5 100644 --- a/tests/models.py +++ b/tests/models.py @@ -24,15 +24,6 @@ class BasicModel(RESTFrameworkModel): ) -class BaseFilterableItem(RESTFrameworkModel): - text = models.CharField(max_length=100) - - -class FilterableItem(BaseFilterableItem): - decimal = models.DecimalField(max_digits=4, decimal_places=2) - date = models.DateField() - - # Models for relations tests # ManyToMany class ManyToManyTarget(RESTFrameworkModel): diff --git a/tests/test_filters.py b/tests/test_filters.py index 6df0a3169..dc5b18068 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -2,125 +2,21 @@ from __future__ import unicode_literals import datetime import unittest -import warnings -from decimal import Decimal import django import pytest -from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.db import models from django.test import TestCase from django.test.utils import override_settings -from django.utils.dateparse import parse_date from django.utils.six.moves import reload_module -from rest_framework import filters, generics, serializers, status -from rest_framework.compat import django_filters, reverse +from rest_framework import filters, generics, serializers from rest_framework.test import APIRequestFactory -from .models import BaseFilterableItem, BasicModel, FilterableItem - factory = APIRequestFactory() -if django_filters: - 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 = (filters.DjangoFilterBackend,) - - # These class are used to test a filter class. - class SeveralFieldsFilter(django_filters.FilterSet): - text = django_filters.CharFilter(lookup_expr='icontains') - decimal = django_filters.NumberFilter(lookup_expr='lt') - date = django_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 = (filters.DjangoFilterBackend,) - - # These classes are used to test a misconfigured filter class. - class MisconfiguredFilter(django_filters.FilterSet): - text = django_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 = (filters.DjangoFilterBackend,) - - class FilterClassDetailView(generics.RetrieveAPIView): - queryset = FilterableItem.objects.all() - serializer_class = FilterableItemSerializer - filter_class = SeveralFieldsFilter - filter_backends = (filters.DjangoFilterBackend,) - - # These classes are used to test base model filter support - class BaseFilterableItemFilter(django_filters.FilterSet): - text = django_filters.CharFilter() - - class Meta: - model = BaseFilterableItem - fields = '__all__' - - # Test the same filter using the deprecated internal FilterSet class. - class BaseFilterableItemFilterWithProxy(filters.FilterSet): - text = django_filters.CharFilter() - - class Meta: - model = BaseFilterableItem - fields = '__all__' - - class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): - queryset = FilterableItem.objects.all() - serializer_class = FilterableItemSerializer - filter_class = BaseFilterableItemFilter - filter_backends = (filters.DjangoFilterBackend,) - - class BaseFilterableItemFilterWithProxyRootView(BaseFilterableItemFilterRootView): - filter_class = BaseFilterableItemFilterWithProxy - - # Regression test for #814 - class FilterFieldsQuerysetView(generics.ListCreateAPIView): - queryset = FilterableItem.objects.all() - serializer_class = FilterableItemSerializer - filter_fields = ['decimal', 'date'] - filter_backends = (filters.DjangoFilterBackend,) - - class GetQuerysetView(generics.ListCreateAPIView): - serializer_class = FilterableItemSerializer - filter_class = SeveralFieldsFilter - filter_backends = (filters.DjangoFilterBackend,) - - def get_queryset(self): - return FilterableItem.objects.all() - - urlpatterns = [ - url(r'^(?P\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 BaseFilterTests(TestCase): def setUp(self): self.original_coreapi = filters.coreapi @@ -142,288 +38,6 @@ class BaseFilterTests(TestCase): assert self.filter_backend.get_schema_fields({}) == [] -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. - """ - - @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() - - assert response.status_code == status.HTTP_200_OK - assert response.data == self.data - - self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) - self.assertIn("'rest_framework.filters.DjangoFilterBackend' is deprecated.", 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() - - assert response.status_code == status.HTTP_200_OK - assert response.data == self.data - assert len(w) == 0 - - @unittest.skipUnless(django_filters, 'django-filter not installed') - def test_backend_mro(self): - class CustomBackend(filters.DjangoFilterBackend): - def filter_queryset(self, request, queryset, view): - assert False, "custom filter_queryset should run" - - class DFFilterFieldsRootView(FilterFieldsRootView): - filter_backends = (CustomBackend,) - - view = DFFilterFieldsRootView.as_view() - request = factory.get('/') - - with pytest.raises(AssertionError, message="custom filter_queryset should run"): - view(request).render() - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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() - assert response.status_code == status.HTTP_200_OK - assert 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - assert 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if parse_date(f['date']) == search_date] - assert response.data == expected_data - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - assert response.data == expected_data - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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. - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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() - assert response.status_code == status.HTTP_200_OK - assert 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal] - assert 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if parse_date(f['date']) > search_date] - assert 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() - assert response.status_code == status.HTTP_200_OK - expected_data = [f for f in self.data if search_text in f['text'].lower()] - assert 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() - assert 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] - assert response.data == expected_data - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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) - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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() - assert response.status_code == status.HTTP_200_OK - assert len(response.data) == 1 - - @unittest.skipUnless(django_filters, 'django-filter not installed') - def test_base_model_filter_with_proxy(self): - """ - The `get_filter_class` model checks should allow base model filters. - """ - view = BaseFilterableItemFilterWithProxyRootView.as_view() - - request = factory.get('/?text=aaa') - response = view(request).render() - assert response.status_code == status.HTTP_200_OK - assert len(response.data) == 1 - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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() - assert response.status_code == status.HTTP_200_OK - - -@override_settings(ROOT_URLCONF='tests.test_filters') -class IntegrationTestDetailFiltering(CommonFilteringTestCase): - """ - Integration tests for filtered detail views. - """ - def _get_url(self, item): - return reverse('detail-view', kwargs=dict(pk=item.pk)) - - @unittest.skipUnless(django_filters, 'django-filter not installed') - 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)) - assert response.status_code == status.HTTP_200_OK - assert 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)}) - assert 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)}) - assert response.status_code == status.HTTP_200_OK - assert 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) - }) - assert response.status_code == status.HTTP_200_OK - assert response.data == valid_item_data - - class SearchFilterModel(models.Model): title = models.CharField(max_length=20) text = models.CharField(max_length=100) @@ -720,42 +334,6 @@ class DjangoFilterOrderingSerializer(serializers.ModelSerializer): 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) - - @unittest.skipUnless(django_filters, 'django-filter not installed') - def test_default_ordering(self): - class DjangoFilterOrderingView(generics.ListAPIView): - serializer_class = DjangoFilterOrderingSerializer - queryset = DjangoFilterOrderingModel.objects.all() - filter_backends = (filters.DjangoFilterBackend,) - filter_fields = ['text'] - ordering = ('-date',) - - view = DjangoFilterOrderingView.as_view() - request = factory.get('/') - response = view(request) - - assert 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 OrderingFilterTests(TestCase): def setUp(self): # Sequence of title/text is: