mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-02-07 15:10:52 +03:00
Merge branch 'master' of github.com:tomchristie/django-rest-framework
This commit is contained in:
commit
222f80f340
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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>.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)),
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
Loading…
Reference in New Issue
Block a user