mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +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.
|
||||
|
||||
[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.
|
||||
|
||||
|
|
|
@ -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>.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 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 = '^' + (' ' * min(whitespace_counts))
|
||||
whitespace_pattern = '^' + (' ' * whitespace_counts)
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
elif tab_counts:
|
||||
whitespace_pattern = '^' + ('\t' * min(whitespace_counts))
|
||||
whitespace_pattern = '^' + ('\t' * tab_counts)
|
||||
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)),
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in New Issue
Block a user