diff --git a/docs/index.md b/docs/index.md
index b9806c544..1760ce916 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -261,7 +261,7 @@ Framework.
For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
-[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
+For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/).
For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter.
diff --git a/docs/topics/funding.md b/docs/topics/funding.md
index 814de0a3c..91099cb3f 100644
--- a/docs/topics/funding.md
+++ b/docs/topics/funding.md
@@ -308,7 +308,7 @@ Our professional and premium plans also include **priority support**. At any tim
Once you've signed up I'll contact you via email and arrange your ad placements on the site.
-For further enquires please contact tom@tomchristie.com.
+For further enquires please contact funding@django-rest-framework.org.
---
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index b0e076203..16bc41f3a 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -17,11 +17,6 @@ from django.template import Context, RequestContext, Template
from django.utils import six
from django.views.generic import View
-try:
- import importlib # Available in Python 3.1+
-except ImportError:
- from django.utils import importlib # Will be removed in Django 1.9
-
try:
from django.urls import (
@@ -312,3 +307,10 @@ def set_many(instance, field, value):
else:
field = getattr(instance, field)
field.set(value)
+
+def include(module, namespace=None, app_name=None):
+ from django.conf.urls import include
+ if django.VERSION < (1,9):
+ return include(module, namespace, app_name)
+ else:
+ return include((module, app_name), namespace)
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 6d9ed2355..b699d7caf 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -18,13 +18,13 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
+from importlib import import_module
from django.conf import settings
from django.test.signals import setting_changed
from django.utils import six
from rest_framework import ISO_8601
-from rest_framework.compat import importlib
DEFAULTS = {
# Base API policies
@@ -174,7 +174,7 @@ def import_from_string(val, setting_name):
# Nod to tastypie's use of importlib.
parts = val.split('.')
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
- module = importlib.import_module(module_path)
+ module = import_module(module_path)
return getattr(module, class_name)
except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 241f94c91..87255bca0 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -114,7 +114,7 @@ if requests is not None:
self.mount('https://', adapter)
def request(self, method, url, *args, **kwargs):
- if ':' not in url:
+ if not url.startswith('http'):
raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url)
return super(RequestsClient, self).request(method, url, *args, **kwargs)
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index 4ea55300e..2ce4ba52d 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,8 +1,8 @@
from __future__ import unicode_literals
-from django.conf.urls import include, url
+from django.conf.urls import url
-from rest_framework.compat import RegexURLResolver
+from rest_framework.compat import RegexURLResolver, include
from rest_framework.settings import api_settings
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index ca5b33c5e..78cb37e56 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -32,23 +32,18 @@ def dedent(content):
unindented text on the initial line.
"""
content = force_text(content)
- whitespace_counts = [
- len(line) - len(line.lstrip(' '))
- for line in content.splitlines()[1:] if line.lstrip()
- ]
- tab_counts = [
- len(line) - len(line.lstrip('\t'))
- for line in content.splitlines()[1:] if line.lstrip()
- ]
+ lines = [line for line in content.splitlines()[1:] if line.lstrip()]
# unindent the content if needed
- if whitespace_counts:
- whitespace_pattern = '^' + (' ' * min(whitespace_counts))
- content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
- elif tab_counts:
- whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
- content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
-
+ if lines:
+ whitespace_counts = min([len(line) - len(line.lstrip(' ')) for line in lines])
+ tab_counts = min([len(line) - len(line.lstrip('\t')) for line in lines])
+ if whitespace_counts:
+ whitespace_pattern = '^' + (' ' * whitespace_counts)
+ content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
+ elif tab_counts:
+ whitespace_pattern = '^' + ('\t' * tab_counts)
+ content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
return content.strip()
diff --git a/tests/test_description.py b/tests/test_description.py
index 08d8bddec..001a3ea21 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -124,4 +124,8 @@ class TestViewNamesAndDescriptions(TestCase):
def test_dedent_tabs():
- assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string'
+ result = 'first string\n\nsecond string'
+ assert dedent(" first string\n\n second string") == result
+ assert dedent("first string\n\n second string") == result
+ assert dedent("\tfirst string\n\n\tsecond string") == result
+ assert dedent("first string\n\n\tsecond string") == result
diff --git a/tests/test_generics.py b/tests/test_generics.py
index c24cda006..59278572e 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -547,3 +547,94 @@ class TestGuardedQueryset(TestCase):
request = factory.get('/')
with pytest.raises(RuntimeError):
view(request).render()
+
+
+class ApiViewsTests(TestCase):
+
+ def test_create_api_view_post(self):
+ class MockCreateApiView(generics.CreateAPIView):
+ def create(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockCreateApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.post('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_destroy_api_view_delete(self):
+ class MockDestroyApiView(generics.DestroyAPIView):
+ def destroy(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockDestroyApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.delete('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_update_api_view_partial_update(self):
+ class MockUpdateApiView(generics.UpdateAPIView):
+ def partial_update(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockUpdateApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.patch('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_retrieve_update_api_view_get(self):
+ class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
+ def retrieve(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockRetrieveUpdateApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.get('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_retrieve_update_api_view_put(self):
+ class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
+ def update(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockRetrieveUpdateApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.put('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_retrieve_update_api_view_patch(self):
+ class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView):
+ def partial_update(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockRetrieveUpdateApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.patch('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_retrieve_destroy_api_view_get(self):
+ class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView):
+ def retrieve(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockRetrieveDestroyUApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.get('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
+
+ def test_retrieve_destroy_api_view_delete(self):
+ class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView):
+ def destroy(self, request, *args, **kwargs):
+ self.called = True
+ self.call_args = (request, args, kwargs)
+ view = MockRetrieveDestroyUApiView()
+ data = ('test request', ('test arg',), {'test_kwarg': 'test'})
+ view.delete('test request', 'test arg', test_kwarg='test')
+ assert view.called is True
+ assert view.call_args == data
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 9f2e1c57c..dd7f70330 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -370,6 +370,13 @@ class TestLimitOffset:
assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type(''))
+ def test_pagination_not_applied_if_limit_or_default_limit_not_set(self):
+ class MockPagination(pagination.LimitOffsetPagination):
+ default_limit = None
+ request = Request(factory.get('/'))
+ queryset = MockPagination().paginate_queryset(self.queryset, request)
+ assert queryset is None
+
def test_single_offset(self):
"""
When the offset is not a multiple of the limit we get some edge cases:
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 99e4391c0..dc3df2e7b 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -4,12 +4,13 @@ import json
from collections import namedtuple
import pytest
-from django.conf.urls import include, url
+from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.test import TestCase, override_settings
from rest_framework import permissions, serializers, viewsets
+from rest_framework.compat import include
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter
@@ -81,7 +82,7 @@ empty_prefix_urls = [
urlpatterns = [
url(r'^non-namespaced/', include(namespaced_router.urls)),
- url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
+ url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')),
url(r'^example/', include(notes_router.urls)),
url(r'^example2/', include(kwarged_notes_router.urls)),
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 47258fdd1..04fa39c0d 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -159,6 +159,32 @@ class TestBaseSerializer:
self.Serializer = ExampleSerializer
+ def test_abstract_methods_raise_proper_errors(self):
+ serializer = serializers.BaseSerializer()
+ with pytest.raises(NotImplementedError):
+ serializer.to_internal_value(None)
+ with pytest.raises(NotImplementedError):
+ serializer.to_representation(None)
+ with pytest.raises(NotImplementedError):
+ serializer.update(None, None)
+ with pytest.raises(NotImplementedError):
+ serializer.create(None)
+
+ def test_access_to_data_attribute_before_validation_raises_error(self):
+ serializer = serializers.BaseSerializer(data={'foo': 'bar'})
+ with pytest.raises(AssertionError):
+ serializer.data
+
+ def test_access_to_errors_attribute_before_validation_raises_error(self):
+ serializer = serializers.BaseSerializer(data={'foo': 'bar'})
+ with pytest.raises(AssertionError):
+ serializer.errors
+
+ def test_access_to_validated_data_attribute_before_validation_raises_error(self):
+ serializer = serializers.BaseSerializer(data={'foo': 'bar'})
+ with pytest.raises(AssertionError):
+ serializer.validated_data
+
def test_serialize_instance(self):
instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
serializer = self.Serializer(instance)
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index 195f3fec1..098b09b65 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -1,8 +1,9 @@
import pytest
-from django.conf.urls import include, url
+from django.conf.urls import url
from django.test import override_settings
from rest_framework import serializers, status, versioning
+from rest_framework.compat import include
from rest_framework.decorators import APIView
from rest_framework.relations import PKOnlyObject
from rest_framework.response import Response
@@ -170,7 +171,7 @@ class TestURLReversing(URLPatternsTestCase):
]
urlpatterns = [
- url(r'^v1/', include(included, namespace='v1')),
+ url(r'^v1/', include(included, namespace='v1', app_name='v1')),
url(r'^another/$', dummy_view, name='another'),
url(r'^(?P[v1|v2]+)/another/$', dummy_view, name='another'),
]
@@ -335,8 +336,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase):
]
urlpatterns = [
- url(r'^v1/', include(included, namespace='v1')),
- url(r'^v2/', include(included, namespace='v2'))
+ url(r'^v1/', include(included, namespace='v1', app_name='v1')),
+ url(r'^v2/', include(included, namespace='v2', app_name='v2'))
]
def setUp(self):
@@ -367,7 +368,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
]
included = [
url(r'^namespaced/(?P\d+)/$', dummy_pk_view, name='namespaced'),
- url(r'^nested/', include(nested, namespace='nested-namespace'))
+ url(r'^nested/', include(nested, namespace='nested-namespace', app_name='nested-namespace'))
]
urlpatterns = [