Merge branch 'master' of github.com:tomchristie/django-rest-framework

This commit is contained in:
Tom Christie 2017-02-03 16:54:23 +00:00
commit 222f80f340
13 changed files with 162 additions and 35 deletions

View File

@ -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. 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. For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter.

View File

@ -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. Once you've signed up I'll contact you via email and arrange your ad placements on the site.
For further enquires please contact <a href=mailto:tom@tomchristie.com>tom@tomchristie.com</a>. For further enquires please contact <a href=mailto:funding@django-rest-framework.org>funding@django-rest-framework.org</a>.
--- ---

View File

@ -17,11 +17,6 @@ from django.template import Context, RequestContext, Template
from django.utils import six from django.utils import six
from django.views.generic import View 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: try:
from django.urls import ( from django.urls import (
@ -312,3 +307,10 @@ def set_many(instance, field, value):
else: else:
field = getattr(instance, field) field = getattr(instance, field)
field.set(value) 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)

View File

@ -18,13 +18,13 @@ REST framework settings, checking for user settings first, then falling
back to the defaults. back to the defaults.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from importlib import import_module
from django.conf import settings from django.conf import settings
from django.test.signals import setting_changed from django.test.signals import setting_changed
from django.utils import six from django.utils import six
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import importlib
DEFAULTS = { DEFAULTS = {
# Base API policies # Base API policies
@ -174,7 +174,7 @@ def import_from_string(val, setting_name):
# Nod to tastypie's use of importlib. # Nod to tastypie's use of importlib.
parts = val.split('.') parts = val.split('.')
module_path, class_name = '.'.join(parts[:-1]), parts[-1] 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) return getattr(module, class_name)
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)

View File

@ -114,7 +114,7 @@ if requests is not None:
self.mount('https://', adapter) self.mount('https://', adapter)
def request(self, method, url, *args, **kwargs): 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) 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) return super(RequestsClient, self).request(method, url, *args, **kwargs)

View File

@ -1,8 +1,8 @@
from __future__ import unicode_literals 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 from rest_framework.settings import api_settings

View File

@ -32,23 +32,18 @@ def dedent(content):
unindented text on the initial line. unindented text on the initial line.
""" """
content = force_text(content) content = force_text(content)
whitespace_counts = [ lines = [line for line in content.splitlines()[1:] if line.lstrip()]
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()
]
# unindent the content if needed # unindent the content if needed
if whitespace_counts: if lines:
whitespace_pattern = '^' + (' ' * min(whitespace_counts)) whitespace_counts = min([len(line) - len(line.lstrip(' ')) for line in lines])
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) tab_counts = min([len(line) - len(line.lstrip('\t')) for line in lines])
elif tab_counts: if whitespace_counts:
whitespace_pattern = '^' + ('\t' * min(whitespace_counts)) whitespace_pattern = '^' + (' ' * whitespace_counts)
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) 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() return content.strip()

View File

@ -124,4 +124,8 @@ class TestViewNamesAndDescriptions(TestCase):
def test_dedent_tabs(): 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

View File

@ -547,3 +547,94 @@ class TestGuardedQueryset(TestCase):
request = factory.get('/') request = factory.get('/')
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
view(request).render() 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

View File

@ -370,6 +370,13 @@ class TestLimitOffset:
assert self.pagination.display_page_controls assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type('')) 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): def test_single_offset(self):
""" """
When the offset is not a multiple of the limit we get some edge cases: When the offset is not a multiple of the limit we get some edge cases:

View File

@ -4,12 +4,13 @@ import json
from collections import namedtuple from collections import namedtuple
import pytest import pytest
from django.conf.urls import include, url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from rest_framework import permissions, serializers, viewsets 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.decorators import detail_route, list_route
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.routers import DefaultRouter, SimpleRouter
@ -81,7 +82,7 @@ empty_prefix_urls = [
urlpatterns = [ urlpatterns = [
url(r'^non-namespaced/', include(namespaced_router.urls)), 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'^example/', include(notes_router.urls)),
url(r'^example2/', include(kwarged_notes_router.urls)), url(r'^example2/', include(kwarged_notes_router.urls)),

View File

@ -159,6 +159,32 @@ class TestBaseSerializer:
self.Serializer = ExampleSerializer 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): def test_serialize_instance(self):
instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'} instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
serializer = self.Serializer(instance) serializer = self.Serializer(instance)

View File

@ -1,8 +1,9 @@
import pytest import pytest
from django.conf.urls import include, url from django.conf.urls import url
from django.test import override_settings from django.test import override_settings
from rest_framework import serializers, status, versioning from rest_framework import serializers, status, versioning
from rest_framework.compat import include
from rest_framework.decorators import APIView from rest_framework.decorators import APIView
from rest_framework.relations import PKOnlyObject from rest_framework.relations import PKOnlyObject
from rest_framework.response import Response from rest_framework.response import Response
@ -170,7 +171,7 @@ class TestURLReversing(URLPatternsTestCase):
] ]
urlpatterns = [ 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'^another/$', dummy_view, name='another'),
url(r'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'), url(r'^(?P<version>[v1|v2]+)/another/$', dummy_view, name='another'),
] ]
@ -335,8 +336,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase):
] ]
urlpatterns = [ urlpatterns = [
url(r'^v1/', include(included, namespace='v1')), url(r'^v1/', include(included, namespace='v1', app_name='v1')),
url(r'^v2/', include(included, namespace='v2')) url(r'^v2/', include(included, namespace='v2', app_name='v2'))
] ]
def setUp(self): def setUp(self):
@ -367,7 +368,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase):
] ]
included = [ included = [
url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'), url(r'^namespaced/(?P<pk>\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 = [ urlpatterns = [