mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-14 05:36:50 +03:00
Merge remote-tracking branch 'upstream/version-3.1' into version-3.1
This commit is contained in:
commit
720d154c83
|
@ -1,17 +1,96 @@
|
||||||
# Django REST framework 3.1
|
# Django REST framework 3.1
|
||||||
|
|
||||||
|
The 3.1 release is an intermediate step in the Kickstarter project releases, and includes a range of new functionality.
|
||||||
|
|
||||||
## Pagination
|
## Pagination
|
||||||
|
|
||||||
|
The pagination API has been improved, making it both easier to use, and more powerful.
|
||||||
|
|
||||||
|
#### New pagination schemes.
|
||||||
|
|
||||||
|
Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default.
|
||||||
|
|
||||||
|
The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. Credit to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject.
|
||||||
|
|
||||||
#### Pagination controls in the browsable API.
|
#### Pagination controls in the browsable API.
|
||||||
|
|
||||||
#### New schemes, including cursor pagination.
|
Paginated results now include controls that render directly in the browsable API. If you're using the page or limit/offset style, then you'll see a page based control displayed in the browsable API.
|
||||||
|
|
||||||
|
**IMAGE**
|
||||||
|
|
||||||
|
The cursor based pagination renders a more simple 'Previous'/'Next' control.
|
||||||
|
|
||||||
|
**IMAGE**
|
||||||
|
|
||||||
#### Support for header-based pagination.
|
#### Support for header-based pagination.
|
||||||
|
|
||||||
|
The pagination API was previously only able to alter the pagination style in the body of the response. The API now supports being able to write pagination information in response headers, making it possible to use pagination schemes that use the `Link` or `Content-Range` headers.
|
||||||
|
|
||||||
|
**TODO**: Link to docs.
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
|
We've made it easier to build versioned APIs. Built-in schemes for versioning include both URL based and Accept header based variations.
|
||||||
|
|
||||||
|
When using a URL based scheme, hyperlinked serializers will resolve relationships to the same API version as used on the incoming request.
|
||||||
|
|
||||||
|
**TODO**: Example.
|
||||||
|
|
||||||
## Internationalization
|
## Internationalization
|
||||||
|
|
||||||
## New fields
|
REST framework now includes a built-in set of translations, and supports internationalized error responses. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header.
|
||||||
|
|
||||||
|
**TODO**: Example.
|
||||||
|
|
||||||
|
**TODO**: Credit.
|
||||||
|
|
||||||
|
## New field types
|
||||||
|
|
||||||
|
Django 1.8's new `ArrayField`, `HStoreField` and `UUIDField` are now all fully supported.
|
||||||
|
|
||||||
|
This work also means that we now have both `serializers.DictField()`, and `serializers.ListField()` types, allowing you to express and validate a wider set of representations.
|
||||||
|
|
||||||
## ModelSerializer API
|
## ModelSerializer API
|
||||||
|
|
||||||
|
The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships.
|
||||||
|
|
||||||
|
**TODO**: Link to docs.
|
||||||
|
|
||||||
|
## Moving packages out of core
|
||||||
|
|
||||||
|
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
|
||||||
|
|
||||||
|
We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework.
|
||||||
|
|
||||||
|
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
|
||||||
|
|
||||||
|
The following packages are now moved out of core and should be separately installed:
|
||||||
|
|
||||||
|
* OAuth - [djangorestframework-oauth](http://jpadilla.github.io/django-rest-framework-oauth/)
|
||||||
|
* XML - [djangorestframework-xml](http://jpadilla.github.io/django-rest-framework-xml)
|
||||||
|
* YAML - [djangorestframework-yaml](http://jpadilla.github.io/django-rest-framework-yaml)
|
||||||
|
* JSONP - [djangorestframework-jsonp](http://jpadilla.github.io/django-rest-framework-jsonp)
|
||||||
|
|
||||||
|
It's worth reiterating that this change in policy shouldn't mean any work in your codebase other than adding a new requirement and modifying some import paths. For example to install XML rendering, you would now do:
|
||||||
|
|
||||||
|
pip install djangorestframework-xml
|
||||||
|
|
||||||
|
And modify your settings, like so:
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_RENDERER_CLASSES': [
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'rest_framework.renderers.BrowsableAPIRenderer',
|
||||||
|
'rest_framework_xml.renderers.XMLRenderer'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# What's next?
|
||||||
|
|
||||||
|
The next focus will be on HTML renderings of API output and will include:
|
||||||
|
|
||||||
|
* HTML form rendering of serializers.
|
||||||
|
* Filtering controls built-in to the browsable API.
|
||||||
|
* An alternative admin-style interface.
|
||||||
|
|
||||||
|
This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release.
|
|
@ -86,8 +86,13 @@ class BasicAuthentication(BaseAuthentication):
|
||||||
Authenticate the userid and password against username and password.
|
Authenticate the userid and password against username and password.
|
||||||
"""
|
"""
|
||||||
user = authenticate(username=userid, password=password)
|
user = authenticate(username=userid, password=password)
|
||||||
if user is None or not user.is_active:
|
|
||||||
|
if user is None:
|
||||||
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
|
raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
|
||||||
|
|
||||||
|
if not user.is_active:
|
||||||
|
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
|
||||||
|
|
||||||
return (user, None)
|
return (user, None)
|
||||||
|
|
||||||
def authenticate_header(self, request):
|
def authenticate_header(self, request):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
|
||||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
|
from django.core.urlresolvers import get_script_prefix, resolve, NoReverseMatch, Resolver404
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
|
@ -167,11 +167,10 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
|
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
|
||||||
self.format = kwargs.pop('format', None)
|
self.format = kwargs.pop('format', None)
|
||||||
|
|
||||||
# We include these simply for dependency injection in tests.
|
# We include this simply for dependency injection in tests.
|
||||||
# We can't add them as class attributes or they would expect an
|
# We can't add it as a class attributes or it would expect an
|
||||||
# implicit `self` argument to be passed.
|
# implicit `self` argument to be passed.
|
||||||
self.reverse = reverse
|
self.reverse = reverse
|
||||||
self.resolve = resolve
|
|
||||||
|
|
||||||
super(HyperlinkedRelatedField, self).__init__(**kwargs)
|
super(HyperlinkedRelatedField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@ -205,6 +204,7 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
|
return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def to_internal_value(self, data):
|
||||||
|
request = self.context.get('request', None)
|
||||||
try:
|
try:
|
||||||
http_prefix = data.startswith(('http:', 'https:'))
|
http_prefix = data.startswith(('http:', 'https:'))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -218,11 +218,18 @@ class HyperlinkedRelatedField(RelatedField):
|
||||||
data = '/' + data[len(prefix):]
|
data = '/' + data[len(prefix):]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
match = self.resolve(data)
|
match = resolve(data)
|
||||||
except Resolver404:
|
except Resolver404:
|
||||||
self.fail('no_match')
|
self.fail('no_match')
|
||||||
|
|
||||||
if match.view_name != self.view_name:
|
try:
|
||||||
|
expected_viewname = request.versioning_scheme.get_versioned_viewname(
|
||||||
|
self.view_name, request
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
expected_viewname = self.view_name
|
||||||
|
|
||||||
|
if match.view_name != expected_viewname:
|
||||||
self.fail('incorrect_match')
|
self.fail('incorrect_match')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Provide reverse functions that return fully qualified URLs
|
Provide urlresolver functions that return fully qualified URLs or view names
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.core.urlresolvers import reverse as django_reverse
|
from django.core.urlresolvers import reverse as django_reverse
|
||||||
|
|
|
@ -122,11 +122,14 @@ class NamespaceVersioning(BaseVersioning):
|
||||||
|
|
||||||
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
|
||||||
if request.version is not None:
|
if request.version is not None:
|
||||||
viewname = request.version + ':' + viewname
|
viewname = self.get_versioned_viewname(viewname, request)
|
||||||
return super(NamespaceVersioning, self).reverse(
|
return super(NamespaceVersioning, self).reverse(
|
||||||
viewname, args, kwargs, request, format, **extra
|
viewname, args, kwargs, request, format, **extra
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_versioned_viewname(self, viewname, request):
|
||||||
|
return request.version + ':' + viewname
|
||||||
|
|
||||||
|
|
||||||
class HostNameVersioning(BaseVersioning):
|
class HostNameVersioning(BaseVersioning):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from django.conf.urls import patterns, url
|
from django.conf.urls import url
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
@ -14,8 +14,7 @@ request = factory.get('/') # Just to ensure we have a request in the serializer
|
||||||
|
|
||||||
dummy_view = lambda request, pk: None
|
dummy_view = lambda request, pk: None
|
||||||
|
|
||||||
urlpatterns = patterns(
|
urlpatterns = [
|
||||||
'',
|
|
||||||
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
|
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
|
||||||
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
|
||||||
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
|
||||||
|
@ -24,7 +23,7 @@ urlpatterns = patterns(
|
||||||
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
|
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
|
||||||
url(r'^onetoonetarget/(?P<pk>[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
|
url(r'^onetoonetarget/(?P<pk>[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
|
||||||
url(r'^nullableonetoonesource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
|
url(r'^nullableonetoonesource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
|
||||||
)
|
]
|
||||||
|
|
||||||
|
|
||||||
# ManyToMany
|
# ManyToMany
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
|
from .utils import UsingURLPatterns
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
|
from rest_framework import serializers
|
||||||
from rest_framework import status, versioning
|
from rest_framework import status, versioning
|
||||||
from rest_framework.decorators import APIView
|
from rest_framework.decorators import APIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.test import APIRequestFactory, APITestCase
|
from rest_framework.test import APIRequestFactory, APITestCase
|
||||||
|
from rest_framework.versioning import NamespaceVersioning
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
class RequestVersionView(APIView):
|
class RequestVersionView(APIView):
|
||||||
|
@ -28,17 +32,8 @@ class RequestInvalidVersionView(APIView):
|
||||||
|
|
||||||
factory = APIRequestFactory()
|
factory = APIRequestFactory()
|
||||||
|
|
||||||
mock_view = lambda request: None
|
dummy_view = lambda request: None
|
||||||
|
dummy_pk_view = lambda request, pk: None
|
||||||
included_patterns = [
|
|
||||||
url(r'^namespaced/$', mock_view, name='another'),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'^v1/', include(included_patterns, namespace='v1')),
|
|
||||||
url(r'^another/$', mock_view, name='another'),
|
|
||||||
url(r'^(?P<version>[^/]+)/another/$', mock_view, name='another')
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class TestRequestVersion:
|
class TestRequestVersion:
|
||||||
|
@ -114,8 +109,17 @@ class TestRequestVersion:
|
||||||
assert response.data == {'version': None}
|
assert response.data == {'version': None}
|
||||||
|
|
||||||
|
|
||||||
class TestURLReversing(APITestCase):
|
class TestURLReversing(UsingURLPatterns, APITestCase):
|
||||||
urls = 'tests.test_versioning'
|
included = [
|
||||||
|
url(r'^namespaced/$', dummy_view, name='another'),
|
||||||
|
url(r'^example/(?P<pk>\d+)/$', dummy_pk_view, name='example-detail')
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^v1/', include(included, namespace='v1')),
|
||||||
|
url(r'^another/$', dummy_view, name='another'),
|
||||||
|
url(r'^(?P<version>[^/]+)/another/$', dummy_view, name='another'),
|
||||||
|
]
|
||||||
|
|
||||||
def test_reverse_unversioned(self):
|
def test_reverse_unversioned(self):
|
||||||
view = ReverseView.as_view()
|
view = ReverseView.as_view()
|
||||||
|
@ -221,3 +225,35 @@ class TestInvalidVersion:
|
||||||
request.resolver_match = FakeResolverMatch
|
request.resolver_match = FakeResolverMatch
|
||||||
response = view(request, version='v3')
|
response = view(request, version='v3')
|
||||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
|
class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase):
|
||||||
|
included = [
|
||||||
|
url(r'^namespaced/(?P<pk>\d+)/$', dummy_view, name='namespaced'),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^v1/', include(included, namespace='v1')),
|
||||||
|
url(r'^v2/', include(included, namespace='v2'))
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestHyperlinkedRelatedField, self).setUp()
|
||||||
|
|
||||||
|
class MockQueryset(object):
|
||||||
|
def get(self, pk):
|
||||||
|
return 'object %s' % pk
|
||||||
|
|
||||||
|
self.field = serializers.HyperlinkedRelatedField(
|
||||||
|
view_name='namespaced',
|
||||||
|
queryset=MockQueryset()
|
||||||
|
)
|
||||||
|
request = factory.get('/')
|
||||||
|
request.versioning_scheme = NamespaceVersioning()
|
||||||
|
request.version = 'v1'
|
||||||
|
self.field._context = {'request': request}
|
||||||
|
|
||||||
|
def test_bug_2489(self):
|
||||||
|
assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3'
|
||||||
|
with pytest.raises(serializers.ValidationError):
|
||||||
|
self.field.to_internal_value('/v2/namespaced/3/')
|
||||||
|
|
|
@ -2,6 +2,30 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
from django.core.urlresolvers import NoReverseMatch
|
||||||
|
|
||||||
|
|
||||||
|
class UsingURLPatterns(object):
|
||||||
|
"""
|
||||||
|
Isolates URL patterns used during testing on the test class itself.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
class MyTestCase(UsingURLPatterns, TestCase):
|
||||||
|
urlpatterns = [
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_something(self):
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
urls = __name__
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
global urlpatterns
|
||||||
|
urlpatterns = self.urlpatterns
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
global urlpatterns
|
||||||
|
urlpatterns = []
|
||||||
|
|
||||||
|
|
||||||
class MockObject(object):
|
class MockObject(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
|
|
Loading…
Reference in New Issue
Block a user