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.
[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.

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.
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.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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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)),

View File

@ -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)

View File

@ -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<version>[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<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 = [