From baa518cd890103173dd18857c609432bd47c6be4 Mon Sep 17 00:00:00 2001
From: Jharrod LaFon
Date: Fri, 5 Sep 2014 15:30:01 -0700
Subject: [PATCH 001/121] Moved OAuth support out of DRF and into a separate
package, per #1767
---
.travis.yml | 3 -
README.md | 2 +-
docs/api-guide/authentication.md | 99 -------
docs/api-guide/permissions.md | 17 --
docs/index.md | 11 +-
requirements-test.txt | 3 -
rest_framework/authentication.py | 183 -------------
rest_framework/compat.py | 51 ----
rest_framework/permissions.py | 28 +-
tests/conftest.py | 20 --
tests/settings.py | 21 --
tests/test_authentication.py | 430 +------------------------------
12 files changed, 6 insertions(+), 862 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index e768e1468..017e72363 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -20,9 +20,6 @@ install:
- pip install django-guardian==1.2.3
- pip install pytest-django==2.6.1
- pip install flake8==2.2.2
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
- "if [[ ${DJANGO} == 'django==1.7' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
diff --git a/README.md b/README.md
index 63513f758..ebc83bf5c 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge useability win for your developers.
-* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
+* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] through the rest-framework-oauth package.
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 343466eee..3d4e0f722 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -247,105 +247,6 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
-## OAuthAuthentication
-
-This authentication uses [OAuth 1.0a][oauth-1.0a] authentication scheme. OAuth 1.0a provides signature validation which provides a reasonable level of security over plain non-HTTPS connections. However, it may also be considered more complicated than OAuth2, as it requires clients to sign their requests.
-
-This authentication class depends on the optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must install these packages and add `oauth_provider` to your `INSTALLED_APPS`:
-
- INSTALLED_APPS = (
- ...
- `oauth_provider`,
- )
-
-Don't forget to run `syncdb` once you've added the package.
-
- python manage.py syncdb
-
-#### Getting started with django-oauth-plus
-
-The OAuthAuthentication class only provides token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing tokens.
-
-The `django-oauth-plus` package provides simple foundation for classic 'three-legged' oauth flow. Please refer to [the documentation][django-oauth-plus] for more details.
-
-## OAuth2Authentication
-
-This authentication uses [OAuth 2.0][rfc6749] authentication scheme. OAuth2 is more simple to work with than OAuth1, and provides much better security than simple token authentication. It is an unauthenticated scheme, and requires you to use an HTTPS connection.
-
-This authentication class depends on the optional [django-oauth2-provider][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS`:
-
- INSTALLED_APPS = (
- ...
- 'provider',
- 'provider.oauth2',
- )
-
-Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting:
-
- 'DEFAULT_AUTHENTICATION_CLASSES': (
- 'rest_framework.authentication.OAuth2Authentication',
- ),
-
-You must also include the following in your root `urls.py` module:
-
- url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
-
-Note that the `namespace='oauth2'` argument is required.
-
-Finally, sync your database.
-
- python manage.py syncdb
- python manage.py migrate
-
----
-
-**Note:** If you use `OAuth2Authentication` in production you must ensure that your API is only available over `https`.
-
----
-
-#### Getting started with django-oauth2-provider
-
-The `OAuth2Authentication` class only provides token verification for requests. It doesn't provide authorization flow for your clients.
-
-The OAuth 2 authorization flow is taken care by the [django-oauth2-provider][django-oauth2-provider] dependency. A walkthrough is given here, but for more details you should refer to [the documentation][django-oauth2-provider-docs].
-
-To get started:
-
-##### 1. Create a client
-
-You can create a client, either through the shell, or by using the Django admin.
-
-Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
-
-##### 2. Request an access token
-
-To request an access token, submit a `POST` request to the url `/oauth2/access_token` with the following fields:
-
-* `client_id` the client id you've just configured at the previous step.
-* `client_secret` again configured at the previous step.
-* `username` the username with which you want to log in.
-* `password` well, that speaks for itself.
-
-You can use the command line to test that your local configuration is working:
-
- curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD" http://localhost:8000/oauth2/access_token/
-
-You should get a response that looks something like this:
-
- {"access_token": "", "scope": "read", "expires_in": 86399, "refresh_token": ""}
-
-##### 3. Access the API
-
-The only thing needed to make the `OAuth2Authentication` class work is to insert the `access_token` you've received in the `Authorization` request header.
-
-The command line to test the authentication looks like:
-
- curl -H "Authorization: Bearer " http://localhost:8000/api/
-
-### Alternative OAuth 2 implementations
-
-Note that [Django OAuth Toolkit][django-oauth-toolkit] is an alternative external package that also includes OAuth 2.0 support for REST framework.
-
---
# Custom authentication
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index e867a4569..a32db4a2d 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -148,21 +148,6 @@ Note that `DjangoObjectPermissions` **does not** require the `django-guardian` p
As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. Note that if you add a custom `view` permission for `GET`, `HEAD` and `OPTIONS` requests, you'll probably also want to consider adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions.
-## TokenHasReadWriteScope
-
-This permission class is intended for use with either of the `OAuthAuthentication` and `OAuth2Authentication` classes, and ties into the scoping that their backends provide.
-
-Requests with a safe methods of `GET`, `OPTIONS` or `HEAD` will be allowed if the authenticated token has read permission.
-
-Requests for `POST`, `PUT`, `PATCH` and `DELETE` will be allowed if the authenticated token has write permission.
-
-This permission class relies on the implementations of the [django-oauth-plus][django-oauth-plus] and [django-oauth2-provider][django-oauth2-provider] libraries, which both provide limited support for controlling the scope of access tokens:
-
-* `django-oauth-plus`: Tokens are associated with a `Resource` class which has a `name`, `url` and `is_readonly` properties.
-* `django-oauth2-provider`: Tokens are associated with a bitwise `scope` attribute, that defaults to providing bitwise values for `read` and/or `write`.
-
-If you require more advanced scoping for your API, such as restricting tokens to accessing a subset of functionality of your API then you will need to provide a custom permission class. See the source of the `django-oauth-plus` or `django-oauth2-provider` package for more details on scoping token access.
-
---
# Custom permissions
@@ -254,8 +239,6 @@ The [REST Condition][rest-condition] package is another extension for building c
[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions
[guardian]: https://github.com/lukaszb/django-guardian
[get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user
-[django-oauth-plus]: http://code.larlet.fr/django-oauth-plus
-[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
[2.2-announcement]: ../topics/2.2-announcement.md
[filtering]: filtering.md
[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
diff --git a/docs/index.md b/docs/index.md
index 1888bfe4b..7dd35feab 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -31,7 +31,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
Some reasons you might want to use REST framework:
* The [Web browseable API][sandbox] is a huge usability win for your developers.
-* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
+* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] through the rest-framework-oauth package.
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
@@ -58,12 +58,9 @@ The following packages are optional:
* [PyYAML][yaml] (3.10+) - YAML content-type support.
* [defusedxml][defusedxml] (0.3+) - XML content-type support.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
-* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
-* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
+* [django-restframework-oauth][django-restframework-oauth] package for OAuth 1.0a and 2.0 support.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
-**Note**: The `oauth2` Python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible.
-
## Installation
Install using `pip`, including any optional packages you want...
@@ -260,9 +257,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
-[oauth2]: https://github.com/simplegeo/python-oauth2
-[django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home
-[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
+[django-restframework-oauth]: https://github.com/jlafon/django-rest-framework-oauth
[django-guardian]: https://github.com/lukaszb/django-guardian
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
diff --git a/requirements-test.txt b/requirements-test.txt
index 411daeba2..a90a1361a 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -9,7 +9,4 @@ markdown>=2.1.0
PyYAML>=3.10
defusedxml>=0.3
django-filter>=0.5.4
-django-oauth-plus>=2.2.1
-oauth2>=1.5.211
-django-oauth2-provider>=0.2.4
Pillow==2.3.0
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index f3fec05ec..ff1c44e0b 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -3,14 +3,9 @@ Provides various authentication policies.
"""
from __future__ import unicode_literals
import base64
-
from django.contrib.auth import authenticate
-from django.core.exceptions import ImproperlyConfigured
from django.middleware.csrf import CsrfViewMiddleware
-from django.conf import settings
from rest_framework import exceptions, HTTP_HEADER_ENCODING
-from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, provider_now, check_nonce
from rest_framework.authtoken.models import Token
@@ -178,181 +173,3 @@ class TokenAuthentication(BaseAuthentication):
def authenticate_header(self, request):
return 'Token'
-
-
-class OAuthAuthentication(BaseAuthentication):
- """
- OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`.
-
- Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- We import it from the `compat` module as `oauth`.
- """
- www_authenticate_realm = 'api'
-
- def __init__(self, *args, **kwargs):
- super(OAuthAuthentication, self).__init__(*args, **kwargs)
-
- if oauth is None:
- raise ImproperlyConfigured(
- "The 'oauth2' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- if oauth_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth-plus' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
- try:
- oauth_request = oauth_provider.utils.get_oauth_request(request)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- if not oauth_request:
- return None
-
- oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES
-
- found = any(param for param in oauth_params if param in oauth_request)
- missing = list(param for param in oauth_params if param not in oauth_request)
-
- if not found:
- # OAuth authentication was not attempted.
- return None
-
- if missing:
- # OAuth was attempted but missing parameters.
- msg = 'Missing parameters: %s' % (', '.join(missing))
- raise exceptions.AuthenticationFailed(msg)
-
- if not self.check_nonce(request, oauth_request):
- msg = 'Nonce check failed'
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
- except oauth_provider.store.InvalidConsumerError:
- msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key')
- raise exceptions.AuthenticationFailed(msg)
-
- if consumer.status != oauth_provider.consts.ACCEPTED:
- msg = 'Invalid consumer key status: %s' % consumer.get_status_display()
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- token_param = oauth_request.get_parameter('oauth_token')
- token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param)
- except oauth_provider.store.InvalidTokenError:
- msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- self.validate_token(request, consumer, token)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
- raise exceptions.AuthenticationFailed(msg)
-
- return (token.user, token)
-
- def authenticate_header(self, request):
- """
- If permission is denied, return a '401 Unauthorized' response,
- with an appropraite 'WWW-Authenticate' header.
- """
- return 'OAuth realm="%s"' % self.www_authenticate_realm
-
- def validate_token(self, request, consumer, token):
- """
- Check the token and raise an `oauth.Error` exception if invalid.
- """
- oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request)
- oauth_server.verify_request(oauth_request, consumer, token)
-
- def check_nonce(self, request, oauth_request):
- """
- Checks nonce of request, and return True if valid.
- """
- oauth_nonce = oauth_request['oauth_nonce']
- oauth_timestamp = oauth_request['oauth_timestamp']
- return check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp)
-
-
-class OAuth2Authentication(BaseAuthentication):
- """
- OAuth 2 authentication backend using `django-oauth2-provider`
- """
- www_authenticate_realm = 'api'
- allow_query_params_token = settings.DEBUG
-
- def __init__(self, *args, **kwargs):
- super(OAuth2Authentication, self).__init__(*args, **kwargs)
-
- if oauth2_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth2-provider' package could not be imported. "
- "It is required for use with the 'OAuth2Authentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
-
- auth = get_authorization_header(request).split()
-
- if len(auth) == 1:
- msg = 'Invalid bearer header. No credentials provided.'
- raise exceptions.AuthenticationFailed(msg)
- elif len(auth) > 2:
- msg = 'Invalid bearer header. Token string should not contain spaces.'
- raise exceptions.AuthenticationFailed(msg)
-
- if auth and auth[0].lower() == b'bearer':
- access_token = auth[1]
- elif 'access_token' in request.POST:
- access_token = request.POST['access_token']
- elif 'access_token' in request.GET and self.allow_query_params_token:
- access_token = request.GET['access_token']
- else:
- return None
-
- return self.authenticate_credentials(request, access_token)
-
- def authenticate_credentials(self, request, access_token):
- """
- Authenticate the request, given the access token.
- """
-
- try:
- token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user')
- # provider_now switches to timezone aware datetime when
- # the oauth2_provider version supports to it.
- token = token.get(token=access_token, expires__gt=provider_now())
- except oauth2_provider.oauth2.models.AccessToken.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.get_username()
- raise exceptions.AuthenticationFailed(msg)
-
- return (user, token)
-
- def authenticate_header(self, request):
- """
- Bearer is the only finalized type currently
-
- Check details on the `OAuth2Authentication.authenticate` method
- """
- return 'Bearer realm="%s"' % self.www_authenticate_realm
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index fa0f0bfb1..bc5719ef8 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -182,57 +182,6 @@ except ImportError:
etree = None
-# OAuth2 is optional
-try:
- # Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- import oauth2 as oauth
-except ImportError:
- oauth = None
-
-
-# OAuthProvider is optional
-try:
- import oauth_provider
- from oauth_provider.store import store as oauth_provider_store
-
- # check_nonce's calling signature in django-oauth-plus changes sometime
- # between versions 2.0 and 2.2.1
- def check_nonce(request, oauth_request, oauth_nonce, oauth_timestamp):
- check_nonce_args = inspect.getargspec(oauth_provider_store.check_nonce).args
- if 'timestamp' in check_nonce_args:
- return oauth_provider_store.check_nonce(
- request, oauth_request, oauth_nonce, oauth_timestamp
- )
- return oauth_provider_store.check_nonce(
- request, oauth_request, oauth_nonce
- )
-
-except (ImportError, ImproperlyConfigured):
- oauth_provider = None
- oauth_provider_store = None
- check_nonce = None
-
-
-# OAuth 2 support is optional
-try:
- import provider as oauth2_provider
- from provider import scope as oauth2_provider_scope
- from provider import constants as oauth2_constants
- if oauth2_provider.__version__ in ('0.2.3', '0.2.4'):
- # 0.2.3 and 0.2.4 are supported version that do not support
- # timezone aware datetimes
- import datetime
- provider_now = datetime.datetime.now
- else:
- # Any other supported version does use timezone aware datetimes
- from django.utils.timezone import now as provider_now
-except ImportError:
- oauth2_provider = None
- oauth2_provider_scope = None
- oauth2_constants = None
- provider_now = None
-
-
# Handle lazy strings across Py2/Py3
from django.utils.functional import Promise
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 29f60d6de..7c4986451 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -3,8 +3,7 @@ Provides a set of pluggable permission policies.
"""
from __future__ import unicode_literals
from django.http import Http404
-from rest_framework.compat import (get_model_name, oauth2_provider_scope,
- oauth2_constants)
+from rest_framework.compat import get_model_name
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
@@ -199,28 +198,3 @@ class DjangoObjectPermissions(DjangoModelPermissions):
return False
return True
-
-
-class TokenHasReadWriteScope(BasePermission):
- """
- The request is authenticated as a user and the token used has the right scope
- """
-
- def has_permission(self, request, view):
- token = request.auth
- read_only = request.method in SAFE_METHODS
-
- if not token:
- return False
-
- if hasattr(token, 'resource'): # OAuth 1
- return read_only or not request.auth.resource.is_readonly
- elif hasattr(token, 'scope'): # OAuth 2
- required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
- return oauth2_provider_scope.check(required, request.auth.scope)
-
- assert False, (
- 'TokenHasReadWriteScope requires either the'
- '`OAuthAuthentication` or `OAuth2Authentication` authentication '
- 'class to be used.'
- )
diff --git a/tests/conftest.py b/tests/conftest.py
index 4b33e19c1..679866215 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -47,26 +47,6 @@ def pytest_configure():
),
)
- try:
- import oauth_provider # NOQA
- import oauth2 # NOQA
- except ImportError:
- pass
- else:
- settings.INSTALLED_APPS += (
- 'oauth_provider',
- )
-
- try:
- import provider # NOQA
- except ImportError:
- pass
- else:
- settings.INSTALLED_APPS += (
- 'provider',
- 'provider.oauth2',
- )
-
# guardian is optional
try:
import guardian # NOQA
diff --git a/tests/settings.py b/tests/settings.py
index 91c9ed09e..6a01669c3 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -101,27 +101,6 @@ INSTALLED_APPS = (
'tests.users',
)
-# OAuth is optional and won't work if there is no oauth_provider & oauth2
-try:
- import oauth_provider # NOQA
- import oauth2 # NOQA
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'oauth_provider',
- )
-
-try:
- import provider # NOQA
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'provider',
- 'provider.oauth2',
- )
-
# guardian is optional
try:
import guardian # NOQA
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 32041f9c1..ece6eff5f 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -3,8 +3,7 @@ from django.conf.urls import patterns, url, include
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.test import TestCase
-from django.utils import six, unittest
-from django.utils.http import urlencode
+from django.utils import six
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
from rest_framework import permissions
@@ -16,17 +15,11 @@ from rest_framework.authentication import (
TokenAuthentication,
BasicAuthentication,
SessionAuthentication,
- OAuthAuthentication,
- OAuth2Authentication
)
from rest_framework.authtoken.models import Token
-from rest_framework.compat import oauth2_provider, oauth2_provider_scope
-from rest_framework.compat import oauth, oauth_provider
from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
import base64
-import time
-import datetime
factory = APIRequestFactory()
@@ -50,37 +43,10 @@ urlpatterns = patterns(
(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
- (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
- (
- r'^oauth-with-scope/$',
- MockView.as_view(
- authentication_classes=[OAuthAuthentication],
- permission_classes=[permissions.TokenHasReadWriteScope]
- )
- ),
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
)
-class OAuth2AuthenticationDebug(OAuth2Authentication):
- allow_query_params_token = True
-
-if oauth2_provider is not None:
- urlpatterns += patterns(
- '',
- url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
- url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
- url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])),
- url(
- r'^oauth2-with-scope-test/$',
- MockView.as_view(
- authentication_classes=[OAuth2Authentication],
- permission_classes=[permissions.TokenHasReadWriteScope]
- )
- )
- )
-
-
class BasicAuthTests(TestCase):
"""Basic authentication"""
urls = 'tests.test_authentication'
@@ -276,400 +242,6 @@ class IncorrectCredentialsTests(TestCase):
self.assertEqual(response.data, {'detail': 'Bad credentials'})
-class OAuthTests(TestCase):
- """OAuth 1.0a authentication"""
- urls = 'tests.test_authentication'
-
- def setUp(self):
- # these imports are here because oauth is optional and hiding them in try..except block or compat
- # could obscure problems if something breaks
- from oauth_provider.models import Consumer, Scope
- from oauth_provider.models import Token as OAuthToken
- from oauth_provider import consts
-
- self.consts = consts
-
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CONSUMER_KEY = 'consumer_key'
- self.CONSUMER_SECRET = 'consumer_secret'
- self.TOKEN_KEY = "token_key"
- self.TOKEN_SECRET = "token_secret"
-
- self.consumer = Consumer.objects.create(
- key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
- name='example', user=self.user, status=self.consts.ACCEPTED
- )
-
- self.scope = Scope.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(
- user=self.user, consumer=self.consumer, scope=self.scope,
- token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET,
- is_approved=True
- )
-
- def _create_authorization_header(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
-
- return req.to_header()["Authorization"]
-
- def _create_authorization_url_parameters(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
- return dict(req)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_passing_oauth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_repeated_nonce_failing_oauth(self):
- """Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- # simulate reply attack auth header containes already used (nonce, timestamp) pair
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_token_removed_failing_oauth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_consumer_status_not_accepted_failing_oauth(self):
- """Ensure POSTing when consumer status is anything other than ACCEPTED fails"""
- for consumer_status in (self.consts.CANCELED, self.consts.PENDING, self.consts.REJECTED):
- self.consumer.status = consumer_status
- self.consumer.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_request_token_failing_oauth(self):
- """Ensure POSTing with unauthorized request token instead of access token fails"""
- self.token.token_type = self.token.REQUEST
- self.token.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_urlencoded_parameters(self):
- """Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
- params = self._create_authorization_url_parameters()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', params, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_url_parameters(self):
- """Ensure GETing with auth in url parameters passes"""
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_hmac_sha1_signature_passes(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_readonly_resource_passing_auth(self):
- """Ensure POSTing with a readonly scope instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.scope.is_readonly = True
- read_only_access_token.scope.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth-with-scope/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_readonly_resource_failing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.scope.is_readonly = True
- read_only_access_token.scope.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_write_resource_passing_auth(self):
- """Ensure POSTing with a write resource succeed"""
- read_write_access_token = self.token
- read_write_access_token.scope.is_readonly = False
- read_write_access_token.scope.save()
- params = self._create_authorization_url_parameters()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth-with-scope/', params, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_consumer_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': 'badconsumerkey'
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_token_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': 'badtokenkey',
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
-
-class OAuth2Tests(TestCase):
- """OAuth 2.0 authentication"""
- urls = 'tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CLIENT_ID = 'client_key'
- self.CLIENT_SECRET = 'client_secret'
- self.ACCESS_TOKEN = "access_token"
- self.REFRESH_TOKEN = "refresh_token"
-
- self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
- client_id=self.CLIENT_ID,
- client_secret=self.CLIENT_SECRET,
- redirect_uri='',
- client_type=0,
- name='example',
- user=None,
- )
-
- self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
- token=self.ACCESS_TOKEN,
- client=self.oauth2_client,
- user=self.user,
- )
- self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
- user=self.user,
- access_token=self.access_token,
- client=self.oauth2_client
- )
-
- def _create_authorization_header(self, token=None):
- return "Bearer {0}".format(token or self.access_token.token)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_type_failing(self):
- """Ensure that a wrong token type lead to the correct HTTP error status code"""
- auth = "Wrong token-type-obsviously"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_format_failing(self):
- """Ensure that a wrong token format lead to the correct HTTP error status code"""
- auth = "Bearer wrong token format"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_failing(self):
- """Ensure that a wrong token lead to the correct HTTP error status code"""
- auth = "Bearer wrong-token"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_missing(self):
- """Ensure that a missing token lead to the correct HTTP error status code"""
- auth = "Bearer"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_passing_auth(self):
- """Ensure GETing form over OAuth with correct client credentials succeed"""
- auth = self._create_authorization_header()
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_passing_auth_url_transport(self):
- """Ensure GETing form over OAuth with correct client credentials in form data succeed"""
- response = self.csrf_client.post(
- '/oauth2-test/',
- data={'access_token': self.access_token.token}
- )
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_passing_auth_url_transport(self):
- """Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True"""
- query = urlencode({'access_token': self.access_token.token})
- response = self.csrf_client.get('/oauth2-test-debug/?%s' % query)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_failing_auth_url_transport(self):
- """Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False"""
- query = urlencode({'access_token': self.access_token.token})
- response = self.csrf_client.get('/oauth2-test/?%s' % query)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_passing_auth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_token_removed_failing_auth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.access_token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_refresh_token_failing_auth(self):
- """Ensure POSTing with refresh token instead of access token fails"""
- auth = self._create_authorization_header(token=self.refresh_token.token)
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_expired_access_token_failing_auth(self):
- """Ensure POSTing with expired access token fails with an 'Invalid token' error"""
- self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
- self.access_token.save()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
- self.assertIn('Invalid token', response.content)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_invalid_scope_failing_auth(self):
- """Ensure POSTing with a readonly scope instead of a write scope fails"""
- read_only_access_token = self.access_token
- read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
- read_only_access_token.save()
- auth = self._create_authorization_header(token=read_only_access_token.token)
- response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_valid_scope_passing_auth(self):
- """Ensure POSTing with a write scope succeed"""
- read_write_access_token = self.access_token
- read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
- read_write_access_token.save()
- auth = self._create_authorization_header(token=read_write_access_token.token)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
-
class FailingAuthAccessedInRenderer(TestCase):
def setUp(self):
class AuthAccessingRenderer(renderers.BaseRenderer):
From afaa52a378705b7f0475d5ece04a2cf49af4b7c2 Mon Sep 17 00:00:00 2001
From: Jharrod LaFon
Date: Fri, 5 Sep 2014 15:42:29 -0700
Subject: [PATCH 002/121] Removes OAuth dependencies from tox configurations
---
tox.ini | 21 ---------------------
1 file changed, 21 deletions(-)
diff --git a/tox.ini b/tox.ini
index d40a70799..a502af972 100644
--- a/tox.ini
+++ b/tox.ini
@@ -45,9 +45,6 @@ basepython = python2.7
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- # django-oauth-plus==2.2.1
- # oauth2==1.5.211
- # django-oauth2-provider==0.2.4
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -81,9 +78,6 @@ basepython = python2.7
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -93,9 +87,6 @@ basepython = python2.6
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -129,9 +120,6 @@ basepython = python2.7
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -141,9 +129,6 @@ basepython = python2.6
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -153,9 +138,6 @@ basepython = python2.7
deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
@@ -165,9 +147,6 @@ basepython = python2.6
deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
django-guardian==1.2.3
Pillow==2.3.0
pytest-django==2.6.1
From 731c8421afe3093a78cdabb9c3cc28fa52cd1c8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sat, 29 Nov 2014 14:43:05 -0400
Subject: [PATCH 003/121] Remove YAML support from core
---
README.md | 49 +++++++++--------
docs/api-guide/parsers.md | 24 +++------
docs/api-guide/renderers.md | 42 ++-------------
docs/api-guide/settings.md | 4 +-
docs/api-guide/testing.md | 4 +-
docs/index.md | 2 -
docs/tutorial/2-requests-and-responses.md | 2 +-
requirements-test.txt | 1 -
rest_framework/compat.py | 7 ---
rest_framework/parsers.py | 25 +--------
rest_framework/renderers.py | 25 +--------
rest_framework/settings.py | 4 +-
rest_framework/utils/encoders.py | 65 +----------------------
tests/test_renderers.py | 57 ++------------------
tests/test_templatetags.py | 13 +----
tox.ini | 1 -
16 files changed, 52 insertions(+), 273 deletions(-)
diff --git a/README.md b/README.md
index c86bb65ff..aafcb29b9 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
Let's take a look at a quick example of using REST framework to build a simple model-backed API for accessing users and groups.
-Startup up a new project like so...
+Startup up a new project like so...
pip install django
pip install djangorestframework
@@ -79,7 +79,7 @@ class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
-
+
# Routers provide a way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
@@ -100,7 +100,7 @@ Add the following to your `settings.py` module:
```python
INSTALLED_APPS = (
... # Make sure to include the default installed apps here.
- 'rest_framework',
+ 'rest_framework',
)
REST_FRAMEWORK = {
@@ -123,10 +123,10 @@ You can also interact with the API using command line tools such as [`curl`](htt
$ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
[
{
- "url": "http://127.0.0.1:8000/users/1/",
- "username": "admin",
- "email": "admin@example.com",
- "is_staff": true,
+ "url": "http://127.0.0.1:8000/users/1/",
+ "username": "admin",
+ "email": "admin@example.com",
+ "is_staff": true,
}
]
@@ -134,10 +134,10 @@ Or to create a new user:
$ curl -X POST -d username=new -d email=new@example.com -d is_staff=false -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
{
- "url": "http://127.0.0.1:8000/users/2/",
- "username": "new",
- "email": "new@example.com",
- "is_staff": false,
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "new",
+ "email": "new@example.com",
+ "is_staff": false,
}
# Documentation & Support
@@ -159,24 +159,24 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
Copyright (c) 2011-2014, Tom Christie
All rights reserved.
-Redistribution and use in source and binary forms, with or without
+Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this
+Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this
-list of conditions and the following disclaimer in the documentation and/or
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -214,7 +214,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[docs]: http://www.django-rest-framework.org/
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
-[pyyaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
[security-mail]: mailto:rest-framework-security@googlegroups.com
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 73e3a7057..1e134c772 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -26,26 +26,26 @@ As an example, if you are sending `json` encoded data using jQuery with the [.aj
## Setting the parsers
-The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content.
+The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `JSON` content.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.JSONParser',
)
}
You can also set the parsers used for an individual view, or viewset,
using the `APIView` class based views.
- from rest_framework.parsers import YAMLParser
+ from rest_framework.parsers import JSONParser
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
"""
- A view that can accept POST requests with YAML content.
+ A view that can accept POST requests with JSON content.
"""
- parser_classes = (YAMLParser,)
+ parser_classes = (JSONParser,)
def post(self, request, format=None):
return Response({'received data': request.data})
@@ -53,10 +53,10 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
@api_view(['POST'])
- @parser_classes((YAMLParser,))
+ @parser_classes((JSONParser,))
def example_view(request, format=None):
"""
- A view that can accept POST requests with YAML content.
+ A view that can accept POST requests with JSON content.
"""
return Response({'received data': request.data})
@@ -70,14 +70,6 @@ Parses `JSON` request content.
**.media_type**: `application/json`
-## YAMLParser
-
-Parses `YAML` request content.
-
-Requires the `pyyaml` package to be installed.
-
-**.media_type**: `application/yaml`
-
## XMLParser
Parses REST framework's default style of `XML` request content.
@@ -161,7 +153,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
## Example
-The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
+The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
class PlainTextParser(BaseParser):
"""
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 035ec1d27..aa8da0886 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -18,11 +18,11 @@ For more information see the documentation on [content negotiation][conneg].
## Setting the renderers
-The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API.
+The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `JSON` as the main media type and also include the self describing API.
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
)
}
@@ -31,15 +31,15 @@ You can also set the renderers used for an individual view, or viewset,
using the `APIView` class based views.
from django.contrib.auth.models import User
- from rest_framework.renderers import JSONRenderer, YAMLRenderer
+ from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
class UserCountView(APIView):
"""
- A view that returns the count of active users, in JSON or YAML.
+ A view that returns the count of active users in JSON.
"""
- renderer_classes = (JSONRenderer, YAMLRenderer)
+ renderer_classes = (JSONRenderer, )
def get(self, request, format=None):
user_count = User.objects.filter(active=True).count()
@@ -113,38 +113,6 @@ The `jsonp` approach is essentially a browser hack, and is [only appropriate for
**.charset**: `utf-8`
-## YAMLRenderer
-
-Renders the request data into `YAML`.
-
-Requires the `pyyaml` package to be installed.
-
-Note that non-ascii characters will be rendered using `\uXXXX` character escape. For example:
-
- unicode black star: "\u2605"
-
-**.media_type**: `application/yaml`
-
-**.format**: `'.yaml'`
-
-**.charset**: `utf-8`
-
-## UnicodeYAMLRenderer
-
-Renders the request data into `YAML`.
-
-Requires the `pyyaml` package to be installed.
-
-Note that non-ascii characters will not be character escaped. For example:
-
- unicode black star: ★
-
-**.media_type**: `application/yaml`
-
-**.format**: `'.yaml'`
-
-**.charset**: `utf-8`
-
## XMLRenderer
Renders REST framework's default style of `XML` response content.
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 9005511b7..623d89fbc 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -12,10 +12,10 @@ For example your project's `settings.py` file might include something like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_PARSER_CLASSES': (
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.JSONParser',
)
}
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index d059fdab5..cd8c7820a 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -255,14 +255,14 @@ The default format used to make test requests may be set using the `TEST_REQUEST
If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting.
-For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file.
+For example, to add support for using `format='html'` in test requests, you might have something like this in your `settings.py` file.
REST_FRAMEWORK = {
...
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
'rest_framework.renderers.JSONRenderer',
- 'rest_framework.renderers.YAMLRenderer'
+ 'rest_framework.renderers.TemplateHTMLRenderer'
)
}
diff --git a/docs/index.md b/docs/index.md
index b5257c734..c2836dbb9 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -54,7 +54,6 @@ REST framework requires the following:
The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
-* [PyYAML][yaml] (3.10+) - YAML content-type support.
* [defusedxml][defusedxml] (0.3+) - XML content-type support.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
@@ -258,7 +257,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[mozilla]: http://www.mozilla.org/en-US/about/
[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
-[yaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
[oauth2]: https://github.com/simplegeo/python-oauth2
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index f377c7122..06a684b17 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -92,7 +92,7 @@ Here is the view for an individual snippet, in the `views.py` module.
This should all feel very familiar - it is not a lot different from working with regular Django views.
-Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.data` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
+Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.data` can handle incoming `json` requests, but it can also handle other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
## Adding optional format suffixes to our URLs
diff --git a/requirements-test.txt b/requirements-test.txt
index 06c8849a8..bd09211ea 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -6,7 +6,6 @@ flake8==2.2.2
# Optional packages
markdown>=2.1.0
-PyYAML>=3.10
defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 5bd85e743..52db96257 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -237,13 +237,6 @@ except ImportError:
apply_markdown = None
-# Yaml is optional
-try:
- import yaml
-except ImportError:
- yaml = None
-
-
# XML is optional
try:
import defusedxml.ElementTree as etree
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index ccb82f03b..e6bb75f6d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -12,7 +12,7 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import etree, yaml, force_text, urlparse
+from rest_framework.compat import etree, force_text, urlparse
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
@@ -65,29 +65,6 @@ class JSONParser(BaseParser):
raise ParseError('JSON parse error - %s' % six.text_type(exc))
-class YAMLParser(BaseParser):
- """
- Parses YAML-serialized data.
- """
-
- media_type = 'application/yaml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as YAML and returns the resulting data.
- """
- assert yaml, 'YAMLParser requires pyyaml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
-
- try:
- data = stream.read().decode(encoding)
- return yaml.safe_load(data)
- except (ValueError, yaml.parser.ParserError) as exc:
- raise ParseError('YAML parse error - %s' % six.text_type(exc))
-
-
class FormParser(BaseParser):
"""
Parser for form data.
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e87d16d0d..a6e4f1bb9 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -19,7 +19,7 @@ from django.utils import six
from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework import exceptions, serializers, status, VERSION
from rest_framework.compat import (
- SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text, yaml
+ SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text
)
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
@@ -189,29 +189,6 @@ class XMLRenderer(BaseRenderer):
xml.characters(smart_text(data))
-class YAMLRenderer(BaseRenderer):
- """
- Renderer which serializes to YAML.
- """
-
- media_type = 'application/yaml'
- format = 'yaml'
- encoder = encoders.SafeDumper
- charset = 'utf-8'
- ensure_ascii = False
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders `data` into serialized YAML.
- """
- assert yaml, 'YAMLRenderer requires pyyaml to be installed'
-
- if data is None:
- return ''
-
- return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
-
-
class TemplateHTMLRenderer(BaseRenderer):
"""
An HTML renderer for use with templates.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 1e8c27fc3..3abc1fe85 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -5,11 +5,11 @@ For example your project's `settings.py` file might look like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.TemplateHTMLRenderer',
)
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.TemplateHTMLRenderer',
)
}
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 4d6bb3a34..2c97f1d7a 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -5,10 +5,9 @@ from __future__ import unicode_literals
from django.db.models.query import QuerySet
from django.utils import six, timezone
from django.utils.functional import Promise
-from rest_framework.compat import force_text, OrderedDict
+from rest_framework.compat import force_text
import datetime
import decimal
-import types
import json
@@ -56,65 +55,3 @@ class JSONEncoder(json.JSONEncoder):
elif hasattr(obj, '__iter__'):
return tuple(item for item in obj)
return super(JSONEncoder, self).default(obj)
-
-
-try:
- import yaml
-except ImportError:
- SafeDumper = None
-else:
- # Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py
- class SafeDumper(yaml.SafeDumper):
- """
- Handles decimals as strings.
- Handles OrderedDicts as usual dicts, but preserves field order, rather
- than the usual behaviour of sorting the keys.
- """
- def represent_decimal(self, data):
- return self.represent_scalar('tag:yaml.org,2002:str', six.text_type(data))
-
- def represent_mapping(self, tag, mapping, flow_style=None):
- value = []
- node = yaml.MappingNode(tag, value, flow_style=flow_style)
- if self.alias_key is not None:
- self.represented_objects[self.alias_key] = node
- best_style = True
- if hasattr(mapping, 'items'):
- mapping = list(mapping.items())
- if not isinstance(mapping, OrderedDict):
- mapping.sort()
- for item_key, item_value in mapping:
- node_key = self.represent_data(item_key)
- node_value = self.represent_data(item_value)
- if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
- best_style = False
- if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style):
- best_style = False
- value.append((node_key, node_value))
- if flow_style is None:
- if self.default_flow_style is not None:
- node.flow_style = self.default_flow_style
- else:
- node.flow_style = best_style
- return node
-
- SafeDumper.add_representer(
- decimal.Decimal,
- SafeDumper.represent_decimal
- )
- SafeDumper.add_representer(
- OrderedDict,
- yaml.representer.SafeRepresenter.represent_dict
- )
- # SafeDumper.add_representer(
- # DictWithMetadata,
- # yaml.representer.SafeRepresenter.represent_dict
- # )
- # SafeDumper.add_representer(
- # OrderedDictWithMetadata,
- # yaml.representer.SafeRepresenter.represent_dict
- # )
- SafeDumper.add_representer(
- types.GeneratorType,
- yaml.representer.SafeRepresenter.represent_list
- )
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 416d7f224..0603f800b 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -9,12 +9,12 @@ from django.test import TestCase
from django.utils import six, unittest
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
-from rest_framework.compat import yaml, etree, StringIO, BytesIO
+from rest_framework.compat import etree, StringIO
from rest_framework.response import Response
from rest_framework.views import APIView
-from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
-from rest_framework.parsers import YAMLParser, XMLParser
+from rest_framework.renderers import BaseRenderer, JSONRenderer, XMLRenderer, \
+ JSONPRenderer, BrowsableAPIRenderer
+from rest_framework.parsers import XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from collections import MutableMapping
@@ -452,55 +452,6 @@ class JSONPRendererTests(TestCase):
)
-if yaml:
- _yaml_repr = 'foo: [bar, baz]\n'
-
- class YAMLRendererTests(TestCase):
- """
- Tests specific to the YAML Renderer
- """
-
- def test_render(self):
- """
- Test basic YAML rendering.
- """
- obj = {'foo': ['bar', 'baz']}
- renderer = YAMLRenderer()
- content = renderer.render(obj, 'application/yaml')
- self.assertEqual(content.decode('utf-8'), _yaml_repr)
-
- def test_render_and_parse(self):
- """
- Test rendering and then parsing returns the original object.
- IE obj -> render -> parse -> obj.
- """
- obj = {'foo': ['bar', 'baz']}
-
- renderer = YAMLRenderer()
- parser = YAMLParser()
-
- content = renderer.render(obj, 'application/yaml')
- data = parser.parse(BytesIO(content))
- self.assertEqual(obj, data)
-
- def test_render_decimal(self):
- """
- Test YAML decimal rendering.
- """
- renderer = YAMLRenderer()
- content = renderer.render({'field': Decimal('111.2')}, 'application/yaml')
- self.assertYAMLContains(content.decode('utf-8'), "field: '111.2'")
-
- def assertYAMLContains(self, content, string):
- self.assertTrue(string in content, '%r not in %r' % (string, content))
-
- def test_proper_encoding(self):
- obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = YAMLRenderer()
- content = renderer.render(obj, 'application/yaml')
- self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
-
-
class XMLRendererTestCase(TestCase):
"""
Tests specific to the XML Renderer
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index b04a937e0..0cee91f19 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -54,7 +54,7 @@ class Issue1386Tests(TestCase):
class URLizerTests(TestCase):
"""
- Test if both JSON and YAML URLs are transformed into links well
+ Test if JSON URLs are transformed into links well
"""
def _urlize_dict_check(self, data):
"""
@@ -73,14 +73,3 @@ class URLizerTests(TestCase):
data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
'"foo_set": [\n "http://api/foos/1/"\n], '
self._urlize_dict_check(data)
-
- def test_yaml_with_url(self):
- """
- Test if YAML URLs are transformed into links well
- """
- data = {}
- data['''{users: 'http://api/users/'}'''] = \
- '''{users: 'http://api/users/'}'''
- data['''foo_set: ['http://api/foos/1/']'''] = \
- '''foo_set: ['http://api/foos/1/']'''
- self._urlize_dict_check(data)
diff --git a/tox.ini b/tox.ini
index d5cb9ef94..edfeb33d8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,7 +24,6 @@ deps =
django-filter==0.7
defusedxml==0.3
markdown>=2.1.0
- PyYAML>=3.10
[testenv:py27-flake8]
deps =
From fe745b96163282e492f17a6b003418b81350333f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sat, 29 Nov 2014 14:55:33 -0400
Subject: [PATCH 004/121] Remove JSONP support from core
---
docs/api-guide/renderers.md | 28 ++----------------
rest_framework/renderers.py | 34 ----------------------
tests/test_renderers.py | 58 +------------------------------------
3 files changed, 3 insertions(+), 117 deletions(-)
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 035ec1d27..a77b9db26 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -49,10 +49,10 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
@api_view(['GET'])
- @renderer_classes((JSONRenderer, JSONPRenderer))
+ @renderer_classes((JSONRenderer,))
def user_count_view(request, format=None):
"""
- A view that returns the count of active users, in JSON or JSONp.
+ A view that returns the count of active users in JSON.
"""
user_count = User.objects.filter(active=True).count()
content = {'user_count': user_count}
@@ -93,26 +93,6 @@ The default JSON encoding style can be altered using the `UNICODE_JSON` and `COM
**.charset**: `None`
-## JSONPRenderer
-
-Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
-
-The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
-
----
-
-**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
-
-The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
-
----
-
-**.media_type**: `application/javascript`
-
-**.format**: `'.jsonp'`
-
-**.charset**: `utf-8`
-
## YAMLRenderer
Renders the request data into `YAML`.
@@ -433,10 +413,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
-[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
-[cors]: http://www.w3.org/TR/cors/
-[cors-docs]: ../topics/ajax-csrf-cors.md
-[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
[testing]: testing.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e87d16d0d..ab6f251c5 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -106,40 +106,6 @@ class JSONRenderer(BaseRenderer):
return ret
-class JSONPRenderer(JSONRenderer):
- """
- Renderer which serializes to json,
- wrapping the json output in a callback function.
- """
-
- media_type = 'application/javascript'
- format = 'jsonp'
- callback_parameter = 'callback'
- default_callback = 'callback'
- charset = 'utf-8'
-
- def get_callback(self, renderer_context):
- """
- Determine the name of the callback to wrap around the json output.
- """
- request = renderer_context.get('request', None)
- params = request and request.query_params or {}
- return params.get(self.callback_parameter, self.default_callback)
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders into jsonp, wrapping the json output in a callback function.
-
- Clients may set the callback function name using a query parameter
- on the URL, for example: ?callback=exampleCallbackName
- """
- renderer_context = renderer_context or {}
- callback = self.get_callback(renderer_context)
- json = super(JSONPRenderer, self).render(data, accepted_media_type,
- renderer_context)
- return callback.encode(self.charset) + b'(' + json + b');'
-
-
class XMLRenderer(BaseRenderer):
"""
Renderer which serializes to XML.
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 416d7f224..15f15dcd2 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -13,7 +13,7 @@ from rest_framework.compat import yaml, etree, StringIO, BytesIO
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
+ XMLRenderer, BrowsableAPIRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
@@ -106,8 +106,6 @@ urlpatterns = patterns(
url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()),
- url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
- url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
@@ -398,60 +396,6 @@ class AsciiJSONRendererTests(TestCase):
self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode('utf-8'))
-class JSONPRendererTests(TestCase):
- """
- Tests specific to the JSONP Renderer
- """
-
- urls = 'tests.test_renderers'
-
- def test_without_callback_with_json_renderer(self):
- """
- Test JSONP rendering with View JSON Renderer.
- """
- resp = self.client.get(
- '/jsonp/jsonrenderer',
- HTTP_ACCEPT='application/javascript'
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(
- resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii')
- )
-
- def test_without_callback_without_json_renderer(self):
- """
- Test JSONP rendering without View JSON Renderer.
- """
- resp = self.client.get(
- '/jsonp/nojsonrenderer',
- HTTP_ACCEPT='application/javascript'
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(
- resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii')
- )
-
- def test_with_callback(self):
- """
- Test JSONP rendering with callback function name.
- """
- callback_func = 'myjsonpcallback'
- resp = self.client.get(
- '/jsonp/nojsonrenderer?callback=' + callback_func,
- HTTP_ACCEPT='application/javascript'
- )
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(
- resp.content,
- ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')
- )
-
-
if yaml:
_yaml_repr = 'foo: [bar, baz]\n'
From 7f9dc736728baf92a3198a7f90bd302fff240373 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jose=CC=81=20Padilla?=
Date: Sat, 29 Nov 2014 14:50:51 -0400
Subject: [PATCH 005/121] Remove XML support from core
---
README.md | 49 ++++++++---------
docs/api-guide/parsers.md | 14 +----
docs/api-guide/renderers.md | 14 -----
docs/index.md | 2 -
requirements-test.txt | 1 -
rest_framework/compat.py | 7 ---
rest_framework/parsers.py | 76 +------------------------
rest_framework/renderers.py | 54 +-----------------
tests/test_parsers.py | 62 +--------------------
tests/test_renderers.py | 107 ++----------------------------------
10 files changed, 32 insertions(+), 354 deletions(-)
diff --git a/README.md b/README.md
index c86bb65ff..83d16030d 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
Let's take a look at a quick example of using REST framework to build a simple model-backed API for accessing users and groups.
-Startup up a new project like so...
+Startup up a new project like so...
pip install django
pip install djangorestframework
@@ -79,7 +79,7 @@ class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
-
+
# Routers provide a way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
@@ -100,7 +100,7 @@ Add the following to your `settings.py` module:
```python
INSTALLED_APPS = (
... # Make sure to include the default installed apps here.
- 'rest_framework',
+ 'rest_framework',
)
REST_FRAMEWORK = {
@@ -123,10 +123,10 @@ You can also interact with the API using command line tools such as [`curl`](htt
$ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
[
{
- "url": "http://127.0.0.1:8000/users/1/",
- "username": "admin",
- "email": "admin@example.com",
- "is_staff": true,
+ "url": "http://127.0.0.1:8000/users/1/",
+ "username": "admin",
+ "email": "admin@example.com",
+ "is_staff": true,
}
]
@@ -134,10 +134,10 @@ Or to create a new user:
$ curl -X POST -d username=new -d email=new@example.com -d is_staff=false -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
{
- "url": "http://127.0.0.1:8000/users/2/",
- "username": "new",
- "email": "new@example.com",
- "is_staff": false,
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "new",
+ "email": "new@example.com",
+ "is_staff": false,
}
# Documentation & Support
@@ -159,24 +159,24 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
Copyright (c) 2011-2014, Tom Christie
All rights reserved.
-Redistribution and use in source and binary forms, with or without
+Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this
+Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this
-list of conditions and the following disclaimer in the documentation and/or
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -215,6 +215,5 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/
[pyyaml]: http://pypi.python.org/pypi/PyYAML
-[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
[security-mail]: mailto:rest-framework-security@googlegroups.com
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 73e3a7057..32819146e 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -78,18 +78,6 @@ Requires the `pyyaml` package to be installed.
**.media_type**: `application/yaml`
-## XMLParser
-
-Parses REST framework's default style of `XML` request content.
-
-Note that the `XML` markup language is typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
-
-If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
-
-Requires the `defusedxml` package to be installed.
-
-**.media_type**: `application/xml`
-
## FormParser
Parses HTML form content. `request.data` will be populated with a `QueryDict` of data.
@@ -161,7 +149,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
## Example
-The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
+The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
class PlainTextParser(BaseParser):
"""
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 035ec1d27..47bf2e601 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -145,20 +145,6 @@ Note that non-ascii characters will not be character escaped. For example:
**.charset**: `utf-8`
-## XMLRenderer
-
-Renders REST framework's default style of `XML` response content.
-
-Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
-
-If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
-
-**.media_type**: `application/xml`
-
-**.format**: `'.xml'`
-
-**.charset**: `utf-8`
-
## TemplateHTMLRenderer
Renders data to HTML, using Django's standard template rendering.
diff --git a/docs/index.md b/docs/index.md
index b5257c734..3b75821bd 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -55,7 +55,6 @@ The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
* [PyYAML][yaml] (3.10+) - YAML content-type support.
-* [defusedxml][defusedxml] (0.3+) - XML content-type support.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
@@ -259,7 +258,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML
-[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
[oauth2]: https://github.com/simplegeo/python-oauth2
[django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home
diff --git a/requirements-test.txt b/requirements-test.txt
index 06c8849a8..75cffb9b7 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -7,7 +7,6 @@ flake8==2.2.2
# Optional packages
markdown>=2.1.0
PyYAML>=3.10
-defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4
django-oauth-plus>=2.2.1
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 5bd85e743..899dd2b48 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -244,13 +244,6 @@ except ImportError:
yaml = None
-# XML is optional
-try:
- import defusedxml.ElementTree as etree
-except ImportError:
- etree = None
-
-
# OAuth2 is optional
try:
# Note: The `oauth2` package actually provides oauth1.0a support. Urg.
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index ccb82f03b..6d0e932bd 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -12,12 +12,10 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import etree, yaml, force_text, urlparse
+from rest_framework.compat import yaml, force_text, urlparse
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
-import datetime
-import decimal
class DataAndFiles(object):
@@ -136,78 +134,6 @@ class MultiPartParser(BaseParser):
raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
-class XMLParser(BaseParser):
- """
- XML parser.
- """
-
- media_type = 'application/xml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as XML and returns the resulting data.
- """
- assert etree, 'XMLParser requires defusedxml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- parser = etree.DefusedXMLParser(encoding=encoding)
- try:
- tree = etree.parse(stream, parser=parser, forbid_dtd=True)
- except (etree.ParseError, ValueError) as exc:
- raise ParseError('XML parse error - %s' % six.text_type(exc))
- data = self._xml_convert(tree.getroot())
-
- return data
-
- def _xml_convert(self, element):
- """
- convert the xml `element` into the corresponding python object
- """
-
- children = list(element)
-
- if len(children) == 0:
- return self._type_convert(element.text)
- else:
- # if the fist child tag is list-item means all children are list-item
- if children[0].tag == "list-item":
- data = []
- for child in children:
- data.append(self._xml_convert(child))
- else:
- data = {}
- for child in children:
- data[child.tag] = self._xml_convert(child)
-
- return data
-
- def _type_convert(self, value):
- """
- Converts the value returned by the XMl parse into the equivalent
- Python type
- """
- if value is None:
- return value
-
- try:
- return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
- except ValueError:
- pass
-
- try:
- return int(value)
- except ValueError:
- pass
-
- try:
- return decimal.Decimal(value)
- except decimal.InvalidOperation:
- pass
-
- return value
-
-
class FileUploadParser(BaseParser):
"""
Parser for file upload data.
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e87d16d0d..dd49ae828 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -16,11 +16,8 @@ from django.http.multipartparser import parse_header
from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
from django.utils import six
-from django.utils.xmlutils import SimplerXMLGenerator
from rest_framework import exceptions, serializers, status, VERSION
-from rest_framework.compat import (
- SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text, yaml
-)
+from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, yaml
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
@@ -140,55 +137,6 @@ class JSONPRenderer(JSONRenderer):
return callback.encode(self.charset) + b'(' + json + b');'
-class XMLRenderer(BaseRenderer):
- """
- Renderer which serializes to XML.
- """
-
- media_type = 'application/xml'
- format = 'xml'
- charset = 'utf-8'
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders `data` into serialized XML.
- """
- if data is None:
- return ''
-
- stream = StringIO()
-
- xml = SimplerXMLGenerator(stream, self.charset)
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
-
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in six.iteritems(data):
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- elif data is None:
- # Don't output any value
- pass
-
- else:
- xml.characters(smart_text(data))
-
-
class YAMLRenderer(BaseRenderer):
"""
Renderer which serializes to YAML.
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 3f2672df0..32fb05955 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -1,15 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from rest_framework.compat import StringIO
from django import forms
from django.core.files.uploadhandler import MemoryFileUploadHandler
from django.test import TestCase
-from django.utils import unittest
-from rest_framework.compat import etree
+from rest_framework.compat import StringIO
from rest_framework.parsers import FormParser, FileUploadParser
-from rest_framework.parsers import XMLParser
-import datetime
class Form(forms.Form):
@@ -31,62 +27,6 @@ class TestFormParser(TestCase):
self.assertEqual(Form(data).is_valid(), True)
-class TestXMLParser(TestCase):
- def setUp(self):
- self._input = StringIO(
- ''
- ''
- '121.0'
- 'dasd'
- ''
- '2011-12-25 12:45:00'
- ''
- )
- self._data = {
- 'field_a': 121,
- 'field_b': 'dasd',
- 'field_c': None,
- 'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }
- self._complex_data_input = StringIO(
- ''
- ''
- '2011-12-25 12:45:00'
- ''
- '1first'
- '2second'
- ''
- 'name'
- ''
- )
- self._complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_parse(self):
- parser = XMLParser()
- data = parser.parse(self._input)
- self.assertEqual(data, self._data)
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_complex_data_parse(self):
- parser = XMLParser()
- data = parser.parse(self._complex_data_input)
- self.assertEqual(data, self._complex_data)
-
-
class TestFileUploadParser(TestCase):
def setUp(self):
class MockRequest(object):
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 416d7f224..1eec37dc3 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -6,19 +6,18 @@ from django.conf.urls import patterns, url, include
from django.core.cache import cache
from django.db import models
from django.test import TestCase
-from django.utils import six, unittest
+from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
-from rest_framework.compat import yaml, etree, StringIO, BytesIO
+from rest_framework.compat import yaml, BytesIO
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
-from rest_framework.parsers import YAMLParser, XMLParser
+ JSONPRenderer, BrowsableAPIRenderer
+from rest_framework.parsers import YAMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from collections import MutableMapping
-import datetime
import json
import pickle
import re
@@ -501,104 +500,6 @@ if yaml:
self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
-class XMLRendererTestCase(TestCase):
- """
- Tests specific to the XML Renderer
- """
-
- _complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- def test_render_string(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 'astring'}, 'application/xml')
- self.assertXMLContains(content, 'astring')
-
- def test_render_integer(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 111}, 'application/xml')
- self.assertXMLContains(content, '111')
-
- def test_render_datetime(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({
- 'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }, 'application/xml')
- self.assertXMLContains(content, '2011-12-25 12:45:00')
-
- def test_render_float(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 123.4}, 'application/xml')
- self.assertXMLContains(content, '123.4')
-
- def test_render_decimal(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
- self.assertXMLContains(content, '111.2')
-
- def test_render_none(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': None}, 'application/xml')
- self.assertXMLContains(content, '')
-
- def test_render_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render(self._complex_data, 'application/xml')
- self.assertXMLContains(content, 'first')
- self.assertXMLContains(content, 'second')
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_render_and_parse_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = StringIO(renderer.render(self._complex_data, 'application/xml'))
-
- parser = XMLParser()
- complex_data_out = parser.parse(content)
- error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
- self.assertEqual(self._complex_data, complex_data_out, error_msg)
-
- def assertXMLContains(self, xml, string):
- self.assertTrue(xml.startswith('\n'))
- self.assertTrue(xml.endswith(''))
- self.assertTrue(string in xml, '%r not in %r' % (string, xml))
-
-
# Tests for caching issue, #346
class CacheRenderTest(TestCase):
"""
From 26131a7aea39bb517393b3b6774372d6aebd6885 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Dec 2014 15:59:11 +0000
Subject: [PATCH 006/121] Fix dependancies
---
requirements.txt | 5 -----
tox.ini | 8 ++++----
2 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/requirements.txt b/requirements.txt
index f282d3baf..474df7168 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,10 +9,5 @@ flake8==2.2.2
# Optional packages
markdown>=2.1.0
-PyYAML>=3.10
-defusedxml>=0.3
django-guardian==1.2.4
django-filter>=0.5.4
-django-oauth-plus>=2.2.1
-oauth2>=1.5.211
-django-oauth2-provider>=0.2.4
diff --git a/tox.ini b/tox.ini
index f129ff3ab..9fb8586c6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,8 @@ envlist =
py27-{flake8,docs},
{py26,py27}-django14,
{py26,py27,py32,py33,py34}-django{15,16},
- {py27,py32,py33,py34}-django{17,master}
+ {py27,py32,py33,py34}-django17,
+ {py27,py32,py33,py34}-djangomaster
[testenv]
commands = ./runtests.py --fast
@@ -15,10 +16,9 @@ deps =
django16: Django==1.6.8
django17: Django==1.7.1
djangomaster: https://github.com/django/django/zipball/master
- {py26,py27}-django{14,15,16,17}: django-guardian==1.2.3
+ django-guardian==1.2.4
pytest-django==2.6.1
- django-filter==0.7
- defusedxml==0.3
+ django-filter==0.9.1
markdown>=2.1.0
[testenv:py27-flake8]
From 0d109c90a74bc575efa6d497a6501aef2b837983 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sat, 13 Dec 2014 18:18:00 -0400
Subject: [PATCH 007/121] Add context to exception handler #2236
Same context as renderers which include: the view,
args, kwargs, and request.
This provides enough contextual information to the
exception handlers to handle errors better.
In a use case like #1671, a custom handler
would allow Sentry to log the request properly.
---
rest_framework/views.py | 5 +++--
tests/test_views.py | 7 ++++++-
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index bc870417f..07e713939 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -46,7 +46,7 @@ def get_view_description(view_cls, html=False):
return description
-def exception_handler(exc):
+def exception_handler(exc, context=None):
"""
Returns the response that should be used for any given exception.
@@ -369,7 +369,8 @@ class APIView(View):
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- response = self.settings.EXCEPTION_HANDLER(exc)
+ context = self.get_renderer_context()
+ response = self.settings.EXCEPTION_HANDLER(exc, context)
if response is None:
raise
diff --git a/tests/test_views.py b/tests/test_views.py
index 77b113ee5..e9b75f065 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -121,7 +121,12 @@ class TestCustomExceptionHandler(TestCase):
def setUp(self):
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
- def exception_handler(exc):
+ def exception_handler(exc, context=None):
+ self.assertTrue('args' in context)
+ self.assertTrue('kwargs' in context)
+ self.assertTrue('request' in context)
+ self.assertTrue('view' in context)
+
return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
api_settings.EXCEPTION_HANDLER = exception_handler
From e8c0766568cb20a5357c5e6823283f0c187b35b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sat, 13 Dec 2014 20:54:35 -0400
Subject: [PATCH 008/121] Support handlers with and without context
---
rest_framework/views.py | 10 ++++++++--
tests/test_views.py | 27 +++++++++++++++++++++------
2 files changed, 29 insertions(+), 8 deletions(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 07e713939..3ece66e68 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -2,6 +2,7 @@
Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
+import inspect
from django.core.exceptions import PermissionDenied
from django.http import Http404
@@ -369,8 +370,13 @@ class APIView(View):
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- context = self.get_renderer_context()
- response = self.settings.EXCEPTION_HANDLER(exc, context)
+ exception_handler = self.settings.EXCEPTION_HANDLER
+
+ if 'context' in inspect.getargspec(exception_handler).args:
+ context = self.get_renderer_context()
+ response = exception_handler(exc, context)
+ else:
+ response = exception_handler(exc)
if response is None:
raise
diff --git a/tests/test_views.py b/tests/test_views.py
index e9b75f065..9952248fc 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -6,6 +6,7 @@ from django.test import TestCase
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
+from rest_framework.request import Request
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
@@ -121,12 +122,7 @@ class TestCustomExceptionHandler(TestCase):
def setUp(self):
self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
- def exception_handler(exc, context=None):
- self.assertTrue('args' in context)
- self.assertTrue('kwargs' in context)
- self.assertTrue('request' in context)
- self.assertTrue('view' in context)
-
+ def exception_handler(exc):
return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
api_settings.EXCEPTION_HANDLER = exception_handler
@@ -151,3 +147,22 @@ class TestCustomExceptionHandler(TestCase):
expected = 'Error!'
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, expected)
+
+ def test_context_exception_handler(self):
+ def exception_handler(exc, context=None):
+ self.assertEqual(context['args'], ())
+ self.assertEqual(context['kwargs'], {})
+ self.assertTrue(isinstance(context['request'], Request))
+ self.assertTrue(isinstance(context['view'], ErrorView))
+
+ return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
+
+ api_settings.EXCEPTION_HANDLER = exception_handler
+
+ view = ErrorView.as_view()
+
+ request = factory.get('/', content_type='application/json')
+ response = view(request)
+ expected = 'Error!'
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data, expected)
From 3f85b476fa2ddb8a205c03cea6684fca257dbd02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 10:15:13 -0400
Subject: [PATCH 009/121] Remove test
---
tests/test_views.py | 20 --------------------
1 file changed, 20 deletions(-)
diff --git a/tests/test_views.py b/tests/test_views.py
index 9952248fc..77b113ee5 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -6,7 +6,6 @@ from django.test import TestCase
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
-from rest_framework.request import Request
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
@@ -147,22 +146,3 @@ class TestCustomExceptionHandler(TestCase):
expected = 'Error!'
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data, expected)
-
- def test_context_exception_handler(self):
- def exception_handler(exc, context=None):
- self.assertEqual(context['args'], ())
- self.assertEqual(context['kwargs'], {})
- self.assertTrue(isinstance(context['request'], Request))
- self.assertTrue(isinstance(context['view'], ErrorView))
-
- return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
-
- api_settings.EXCEPTION_HANDLER = exception_handler
-
- view = ErrorView.as_view()
-
- request = factory.get('/', content_type='application/json')
- response = view(request)
- expected = 'Error!'
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(response.data, expected)
From 478c8d724b846b370c897548f8ee89f1128e12c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 10:16:52 -0400
Subject: [PATCH 010/121] Update docs
---
docs/api-guide/exceptions.md | 4 ++--
docs/api-guide/settings.md | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index 467ad9709..31a8431bc 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -51,10 +51,10 @@ In order to alter the style of the response, you could write the following custo
from rest_framework.views import exception_handler
- def custom_exception_handler(exc):
+ def custom_exception_handler(exc, context):
# Call REST framework's default exception handler first,
# to get the standard error response.
- response = exception_handler(exc)
+ response = exception_handler(exc, context)
# Now add the HTTP status code to the response.
if response is not None:
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 9005511b7..2c4f84237 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -393,7 +393,7 @@ This setting can be changed to support error responses other than the default `{
This should be a function with the following signature:
- exception_handler(exc)
+ exception_handler(exc, context)
* `exc`: The exception.
From fd003fcefaee964e744ded0aec1ae76715889378 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 15:03:20 -0400
Subject: [PATCH 011/121] Add pending deprecation warning message
---
rest_framework/views.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 3ece66e68..37889d1b4 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -3,6 +3,7 @@ Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
import inspect
+import warnings
from django.core.exceptions import PermissionDenied
from django.http import Http404
@@ -370,13 +371,16 @@ class APIView(View):
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- exception_handler = self.settings.EXCEPTION_HANDLER
-
- if 'context' in inspect.getargspec(exception_handler).args:
- context = self.get_renderer_context()
- response = exception_handler(exc, context)
+ if len(inspect.getargspec(self.settings.EXCEPTION_HANDLER).args) == 1:
+ warnings.warn(
+ 'The `exception_handler(exc)` call signature is deprecated. '
+ 'Use `exception_handler(exc, context) instead.',
+ PendingDeprecationWarning
+ )
+ response = self.settings.EXCEPTION_HANDLER(exc)
else:
- response = exception_handler(exc)
+ context = self.get_renderer_context()
+ response = self.settings.EXCEPTION_HANDLER(exc, context)
if response is None:
raise
From 89e9fc98d6e7407e6f7715fa2680df7c94221105 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 15:20:44 -0400
Subject: [PATCH 012/121] Reuse exception_handler variable throughout
---
rest_framework/views.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 37889d1b4..c2e19bf42 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -371,16 +371,18 @@ class APIView(View):
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- if len(inspect.getargspec(self.settings.EXCEPTION_HANDLER).args) == 1:
+ exception_handler = self.settings.EXCEPTION_HANDLER
+
+ if len(inspect.getargspec(exception_handler).args) == 1:
warnings.warn(
'The `exception_handler(exc)` call signature is deprecated. '
'Use `exception_handler(exc, context) instead.',
PendingDeprecationWarning
)
- response = self.settings.EXCEPTION_HANDLER(exc)
+ response = exception_handler(exc)
else:
context = self.get_renderer_context()
- response = self.settings.EXCEPTION_HANDLER(exc, context)
+ response = exception_handler(exc, context)
if response is None:
raise
From 26c223a34f7e0cc21d37c6302e53d547dae252dd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 16:43:58 -0400
Subject: [PATCH 013/121] Add get_exception_handler_context()
---
rest_framework/views.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index c2e19bf42..80a13a1a9 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -186,6 +186,18 @@ class APIView(View):
'request': getattr(self, 'request', None)
}
+ def get_exception_handler_context(self):
+ """
+ Returns a dict that is passed through to EXCEPTION_HANDLER,
+ as the `context` argument.
+ """
+ return {
+ 'view': self,
+ 'args': getattr(self, 'args', ()),
+ 'kwargs': getattr(self, 'kwargs', {}),
+ 'request': getattr(self, 'request', None)
+ }
+
def get_view_name(self):
"""
Return the view name, as used in OPTIONS responses and in the
@@ -381,7 +393,7 @@ class APIView(View):
)
response = exception_handler(exc)
else:
- context = self.get_renderer_context()
+ context = self.get_exception_handler_context()
response = exception_handler(exc, context)
if response is None:
From 4ebd8770b94ecb8fe8fb41fe8daa4309b33b9952 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Sun, 14 Dec 2014 20:47:33 -0400
Subject: [PATCH 014/121] Update excepteion_handler signature
---
rest_framework/views.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 80a13a1a9..b39724c2f 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -48,7 +48,7 @@ def get_view_description(view_cls, html=False):
return description
-def exception_handler(exc, context=None):
+def exception_handler(exc, context):
"""
Returns the response that should be used for any given exception.
From 5e7c9687c7e11b6adfe2fc534eb0504e67ca9fc9 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Dec 2014 09:13:02 +0000
Subject: [PATCH 015/121] First pass at serializer repr bug
---
rest_framework/utils/representation.py | 3 ++-
tests/test_serializer.py | 17 +++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 3f17a8b9b..0fdb4775d 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -2,6 +2,7 @@
Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
+from __future__ import unicode_literals
from django.db import models
from django.utils.encoding import force_text
from django.utils.functional import Promise
@@ -24,7 +25,7 @@ def smart_repr(value):
if isinstance(value, Promise) and value._delegate_text:
value = force_text(value)
- value = repr(value)
+ value = repr(value).decode('utf-8')
# Representations like u'help text'
# should simply be presented as 'help text'
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 56b390956..48fcc83bb 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,3 +1,4 @@
+# coding: utf-8
from __future__ import unicode_literals
from rest_framework import serializers
import pytest
@@ -197,3 +198,19 @@ class TestIncorrectlyConfigured:
"The serializer field might be named incorrectly and not match any attribute or key on the `ExampleObject` instance.\n"
"Original exception text was:"
)
+
+
+class TestUnicodeRepr:
+ def test_unicode_repr(self):
+ class ExampleSerializer(serializers.Serializer):
+ example = serializers.CharField()
+
+ class ExampleObject:
+ def __init__(self):
+ self.example = '한국'
+ def __repr__(self):
+ return self.example.encode('utf8')
+
+ instance = ExampleObject()
+ serializer = ExampleSerializer(instance)
+ repr(serializer)
From 6e51e4f5cdec4f4580360a487d7bf5ebdef08709 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 16 Dec 2014 15:34:19 +0000
Subject: [PATCH 016/121] Versioning first pass
---
docs/api-guide/versioning.md | 9 +++
rest_framework/reverse.py | 12 ++++
rest_framework/settings.py | 7 ++-
rest_framework/versioning.py | 96 ++++++++++++++++++++++++++++++++
rest_framework/views.py | 27 +++++++--
tests/test_versioning.py | 104 +++++++++++++++++++++++++++++++++++
6 files changed, 248 insertions(+), 7 deletions(-)
create mode 100644 docs/api-guide/versioning.md
create mode 100644 rest_framework/versioning.py
create mode 100644 tests/test_versioning.py
diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md
new file mode 100644
index 000000000..df8148941
--- /dev/null
+++ b/docs/api-guide/versioning.md
@@ -0,0 +1,9 @@
+source: versioning.py
+
+# Versioning
+
+> Versioning an interface is just a "polite" way to kill deployed clients.
+>
+> — [Roy Fielding][cite].
+
+[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
\ No newline at end of file
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index a74e8aa2d..8fcca55ba 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -8,6 +8,18 @@ from django.utils.functional import lazy
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ """
+ If versioning is being used then we pass any `reverse` calls through
+ to the versioning scheme instance, so that the resulting URL
+ can be modified if needed.
+ """
+ scheme = getattr(request, 'versioning_scheme', None)
+ if scheme is not None:
+ return scheme.reverse(viewname, args, kwargs, request, format, **extra)
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+
+def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 0aac6d43e..b17f5fccf 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -46,6 +46,7 @@ DEFAULTS = {
'DEFAULT_THROTTLE_CLASSES': (),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
+ 'DEFAULT_VERSIONING_CLASS': None,
# Generic view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
@@ -124,7 +125,7 @@ IMPORT_STRINGS = (
'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_METADATA_CLASS',
- 'DEFAULT_MODEL_SERIALIZER_CLASS',
+ 'DEFAULT_VERSIONING_CLASS',
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER',
@@ -141,7 +142,9 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
- if isinstance(val, six.string_types):
+ if val is None:
+ return None
+ elif isinstance(val, six.string_types):
return import_from_string(val, setting_name)
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
new file mode 100644
index 000000000..2ca8efff4
--- /dev/null
+++ b/rest_framework/versioning.py
@@ -0,0 +1,96 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from rest_framework.reverse import _reverse
+from rest_framework.utils.mediatypes import _MediaType
+import re
+
+
+class BaseVersioning(object):
+ def determine_version(self, request, *args, **kwargs):
+ msg = '{cls}.determine_version() must be implemented.'
+ raise NotImplemented(msg.format(
+ cls=self.__class__.__name__
+ ))
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+
+class QueryParameterVersioning(BaseVersioning):
+ """
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ default_version = None
+ version_param = 'version'
+
+ def determine_version(self, request, *args, **kwargs):
+ return request.query_params.get(self.version_param)
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ url = super(QueryParameterVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **kwargs
+ )
+ if request.version is not None:
+ return replace_query_param(url, self.version_param, request.version)
+ return url
+
+
+class HostNameVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+ """
+ default_version = None
+ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
+
+ def determine_version(self, request, *args, **kwargs):
+ hostname, seperator, port = request.get_host().partition(':')
+ match = self.hostname_regex.match(hostname)
+ if not match:
+ return self.default_version
+ return match.group(1)
+
+ # We don't need to implement `reverse`, as the hostname will already be
+ # preserved as part of the standard `reverse` implementation.
+
+
+class AcceptHeaderVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json; version=1.0
+ """
+ default_version = None
+ version_param = 'version'
+
+ def determine_version(self, request, *args, **kwargs):
+ media_type = _MediaType(request.accepted_media_type)
+ return media_type.params.get(self.version_param, self.default_version)
+
+ # We don't need to implement `reverse`, as the versioning is based
+ # on the `Accept` header, not on the request URL.
+
+
+class URLPathVersioning(BaseVersioning):
+ """
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ default_version = None
+ version_param = 'version'
+
+ def determine_version(self, request, *args, **kwargs):
+ return kwargs.get(self.version_param, self.default_version)
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ kwargs = {} if (kwargs is None) else kwargs
+ kwargs[self.version_param] = request.version
+
+ return super(URLPathVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **kwargs
+ )
diff --git a/rest_framework/views.py b/rest_framework/views.py
index b39724c2f..12bb78bd9 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -95,6 +95,7 @@ class APIView(View):
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
+ versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
# Allow dependency injection of other settings to make testing easier.
settings = api_settings
@@ -314,6 +315,16 @@ class APIView(View):
if not throttle.allow_request(request, self):
self.throttled(request, throttle.wait())
+ def determine_version(self, request, *args, **kwargs):
+ """
+ If versioning is being used, then determine any API version for the
+ incoming request. Returns a two-tuple of (version, versioning_scheme)
+ """
+ if self.versioning_class is None:
+ return (None, None)
+ scheme = self.versioning_class()
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
# Dispatch methods
def initialize_request(self, request, *args, **kwargs):
@@ -322,11 +333,13 @@ class APIView(View):
"""
parser_context = self.get_parser_context(request)
- return Request(request,
- parsers=self.get_parsers(),
- authenticators=self.get_authenticators(),
- negotiator=self.get_content_negotiator(),
- parser_context=parser_context)
+ return Request(
+ request,
+ parsers=self.get_parsers(),
+ authenticators=self.get_authenticators(),
+ negotiator=self.get_content_negotiator(),
+ parser_context=parser_context
+ )
def initial(self, request, *args, **kwargs):
"""
@@ -343,6 +356,10 @@ class APIView(View):
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
+ # Determine the API version, if versioning is in use.
+ version, scheme = self.determine_version(request, *args, **kwargs)
+ request.version, request.versioning_scheme = version, scheme
+
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
new file mode 100644
index 000000000..d90b29a1b
--- /dev/null
+++ b/tests/test_versioning.py
@@ -0,0 +1,104 @@
+from django.conf.urls import url
+from rest_framework import versioning
+from rest_framework.decorators import APIView
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework.test import APIRequestFactory, APITestCase
+
+
+class RequestVersionView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+class ReverseView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'url': reverse('another', request=request)})
+
+
+factory = APIRequestFactory()
+
+mock_view = lambda request: None
+
+urlpatterns = [
+ url(r'^another/$', mock_view, name='another')
+]
+
+
+class TestRequestVersion:
+ def test_unversioned(self):
+ view = RequestVersionView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/1.2.3/endpoint/')
+ response = view(request, version='1.2.3')
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+
+class TestURLReversing(APITestCase):
+ urls = 'tests.test_versioning'
+
+ def test_reverse_unversioned(self):
+ view = ReverseView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'url': 'http://v1.example.org/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
From 4e91ec61339838426e246e20ef062c963a78c4e1 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 16 Dec 2014 16:14:08 +0000
Subject: [PATCH 017/121] Added NamespaceVersioning
---
rest_framework/versioning.py | 31 +++++++++++++++--
tests/test_versioning.py | 67 ++++++++++++++++++++++++++++++++++--
2 files changed, 94 insertions(+), 4 deletions(-)
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 2ca8efff4..42df8b2c0 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -1,6 +1,7 @@
# coding: utf-8
from __future__ import unicode_literals
from rest_framework.reverse import _reverse
+from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType
import re
@@ -30,7 +31,7 @@ class QueryParameterVersioning(BaseVersioning):
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
url = super(QueryParameterVersioning, self).reverse(
- viewname, args, kwargs, request, format, **kwargs
+ viewname, args, kwargs, request, format, **extra
)
if request.version is not None:
return replace_query_param(url, self.version_param, request.version)
@@ -92,5 +93,31 @@ class URLPathVersioning(BaseVersioning):
kwargs[self.version_param] = request.version
return super(URLPathVersioning, self).reverse(
- viewname, args, kwargs, request, format, **kwargs
+ viewname, args, kwargs, request, format, **extra
+ )
+
+
+class NamespaceVersioning(BaseVersioning):
+ """
+ To the client this is the same style as `URLPathVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL namespaces to determine the version.
+
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ default_version = None
+
+ def determine_version(self, request, *args, **kwargs):
+ resolver_match = getattr(request, 'resolver_match', None)
+ if (resolver_match is None or not resolver_match.namespace):
+ return self.default_version
+ return resolver_match.namespace
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ viewname = request.version + ':' + viewname
+ return super(NamespaceVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
)
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index d90b29a1b..eaac5dfb3 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -1,4 +1,4 @@
-from django.conf.urls import url
+from django.conf.urls import include, url
from rest_framework import versioning
from rest_framework.decorators import APIView
from rest_framework.response import Response
@@ -10,6 +10,7 @@ class RequestVersionView(APIView):
def get(self, request, *args, **kwargs):
return Response({'version': request.version})
+
class ReverseView(APIView):
def get(self, request, *args, **kwargs):
return Response({'url': reverse('another', request=request)})
@@ -19,8 +20,14 @@ factory = APIRequestFactory()
mock_view = lambda request: None
+included_patterns = [
+ url(r'^namespaced/$', mock_view, name='another'),
+]
+
urlpatterns = [
- url(r'^another/$', mock_view, name='another')
+ url(r'^v1/', include(included_patterns, namespace='v1')),
+ url(r'^another/$', mock_view, name='another'),
+ url(r'^(?P[^/]+)/another/$', mock_view, name='another')
]
@@ -80,6 +87,22 @@ class TestRequestVersion:
response = view(request)
assert response.data == {'version': None}
+ def test_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
class TestURLReversing(APITestCase):
urls = 'tests.test_versioning'
@@ -91,6 +114,18 @@ class TestURLReversing(APITestCase):
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
+ def test_reverse_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v1')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/?version=v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
def test_reverse_host_name_versioning(self):
scheme = versioning.HostNameVersioning
view = ReverseView.as_view(versioning_class=scheme)
@@ -102,3 +137,31 @@ class TestURLReversing(APITestCase):
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/namespaced/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
From fe9647ce92b61b57dc64604241352bf269d65af7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 16 Dec 2014 16:37:32 +0000
Subject: [PATCH 018/121] AcceptHeaderVersioning to return unicode strings.
---
rest_framework/compat.py | 13 +++++++++----
rest_framework/versioning.py | 29 ++++++++++++++++++++++++++++-
2 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index c5242343e..3c8fb0da4 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -5,15 +5,13 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
-
-import inspect
-
from django.core.exceptions import ImproperlyConfigured
+from django.conf import settings
from django.utils.encoding import force_text
from django.utils.six.moves.urllib import parse as urlparse
-from django.conf import settings
from django.utils import six
import django
+import inspect
def unicode_repr(instance):
@@ -33,6 +31,13 @@ def unicode_to_repr(value):
return value
+def unicode_http_header(value):
+ # Coerce HTTP header value to unicode.
+ if isinstance(value, six.binary_type):
+ return value.decode('iso-8859-1')
+ return value
+
+
# OrderedDict only available in Python 2.7.
# This will always be the case in Django 1.7 and above, as these versions
# no longer support Python 2.6.
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 42df8b2c0..9a27cb081 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -1,5 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
+from rest_framework.compat import unicode_http_header
from rest_framework.reverse import _reverse
from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType
@@ -69,7 +70,8 @@ class AcceptHeaderVersioning(BaseVersioning):
def determine_version(self, request, *args, **kwargs):
media_type = _MediaType(request.accepted_media_type)
- return media_type.params.get(self.version_param, self.default_version)
+ version = media_type.params.get(self.version_param, self.default_version)
+ return unicode_http_header(version)
# We don't need to implement `reverse`, as the versioning is based
# on the `Accept` header, not on the request URL.
@@ -77,6 +79,17 @@ class AcceptHeaderVersioning(BaseVersioning):
class URLPathVersioning(BaseVersioning):
"""
+ To the client this is the same style as `NamespaceVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL keyword arguments to determine the version.
+
+ An example URL conf for two views that accept two different versions.
+
+ urlpatterns = [
+ url(r'^(?P{v1,v2})/users/$', users_list, name='users-list'),
+ url(r'^(?P{v1,v2})/users/(?P[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
GET /1.0/something/ HTTP/1.1
Host: example.com
Accept: application/json
@@ -103,6 +116,20 @@ class NamespaceVersioning(BaseVersioning):
The difference is in the backend - this implementation uses
Django's URL namespaces to determine the version.
+ An example URL conf that is namespaced into two seperate versions
+
+ # users/urls.py
+ urlpatterns = [
+ url(r'^/users/$', users_list, name='users-list'),
+ url(r'^/users/(?P[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
+ # urls.py
+ urlpatterns = [
+ url(r'^v1/', include('users.urls', namespace='v1')),
+ url(r'^v2/', include('users.urls', namespace='v2'))
+ ]
+
GET /1.0/something/ HTTP/1.1
Host: example.com
Accept: application/json
From 70bd3a32f7cf57543e8ec08fddf001a718e40c7f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 16 Dec 2014 20:01:01 +0000
Subject: [PATCH 019/121] Minor comment tweak
---
rest_framework/versioning.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 9a27cb081..223d0f613 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -56,7 +56,7 @@ class HostNameVersioning(BaseVersioning):
return match.group(1)
# We don't need to implement `reverse`, as the hostname will already be
- # preserved as part of the standard `reverse` implementation.
+ # preserved as part of the REST framework `reverse` implementation.
class AcceptHeaderVersioning(BaseVersioning):
From 05a6eaec8aebdca2248b9e1069a15769fd85a480 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 17 Dec 2014 12:41:46 +0000
Subject: [PATCH 020/121] More docs, plus 'ALLOWED_VERSIONS' setting.
---
docs/api-guide/exceptions.md | 16 +++
docs/api-guide/settings.md | 22 ++++
docs/api-guide/versioning.md | 195 ++++++++++++++++++++++++++++++++++-
docs/index.md | 2 +
mkdocs.yml | 1 +
rest_framework/exceptions.py | 5 +
rest_framework/settings.py | 5 +
rest_framework/versioning.py | 120 ++++++++++++---------
tests/test_versioning.py | 60 ++++++++++-
9 files changed, 375 insertions(+), 51 deletions(-)
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index 31a8431bc..50bd14dd2 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -128,6 +128,14 @@ Raised when an authenticated request fails the permission checks.
By default this exception results in a response with the HTTP status code "403 Forbidden".
+## NotFound
+
+**Signature:** `NotFound(detail=None)`
+
+Raised when a resource does not exists at the given URL. This exception is equivalent to the standard `Http404` Django exception.
+
+By default this exception results in a response with the HTTP status code "404 Not Found".
+
## MethodNotAllowed
**Signature:** `MethodNotAllowed(method, detail=None)`
@@ -136,6 +144,14 @@ Raised when an incoming request occurs that does not map to a handler method on
By default this exception results in a response with the HTTP status code "405 Method Not Allowed".
+## NotAcceptable
+
+**Signature:** `NotAcceptable(detail=None)`
+
+Raised when an incoming request occurs with an `Accept` header that cannot be satisfied by any of the available renderers.
+
+By default this exception results in a response with the HTTP status code "406 Not Acceptable".
+
## UnsupportedMediaType
**Signature:** `UnsupportedMediaType(media_type, detail=None)`
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 9efeda7fa..5af429d16 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -166,6 +166,28 @@ Default: `ordering`
---
+## Versioning settings
+
+#### DEFAULT_VERSION
+
+The value that should be used for `request.version` when no versioning information is present.
+
+Default: `None`
+
+#### ALLOWED_VERSIONS
+
+If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set.
+
+Default: `None`
+
+#### VERSION_PARAMETER
+
+The string that should used for any versioning parameters, such as in the media type or URL query parameters.
+
+Default: `'version'`
+
+---
+
## Authentication settings
*The following settings control the behavior of unauthenticated requests.*
diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md
index df8148941..92380cc0e 100644
--- a/docs/api-guide/versioning.md
+++ b/docs/api-guide/versioning.md
@@ -6,4 +6,197 @@ source: versioning.py
>
> — [Roy Fielding][cite].
-[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
\ No newline at end of file
+API versioning allows you to alter behavior between different clients. REST framework provides for a number of different versioning schemes.
+
+Versioning is determined by the incoming client request, and may either be based on the request URL, or based on the request headers.
+
+## Versioning with REST framework
+
+When API versioning is enabled, the `request.version` attribute will contain a string that corresponds to the version requested in the incoming client request.
+
+By default, versioning is not enabled, and `request.version` will always return `None`.
+
+#### Varying behavior based on the version
+
+How you vary the API behavior is up to you, but one example you might typically want is to switch to a different serialization style in a newer version. For example:
+
+ def get_serializer_class(self):
+ if self.request.version == 'v1':
+ return AccountSerializerVersion1
+ return AccountSerializer
+
+#### Reversing URLs for versioned APIs
+
+The `reverse` function included by REST framework ties in with the versioning scheme. You need to make sure to include the current `request` as a keyword argument, like so.
+
+ reverse('bookings-list', request=request)
+
+The above function will apply any URL transformations appropriate to the request version. For example:
+
+* If `NamespacedVersioning` was being used, and the API version was 'v1', then the URL lookup used would be `'v1:bookings-list'`, which might resolve to a URL like `http://example.org/v1/bookings/`.
+* If `QueryParameterVersioning` was being used, and the API version was `1.0`, then the returned URL might be something like `http://example.org/bookings/?version=1.0`
+
+#### Versioned APIs and hyperlinked serializers
+
+When using hyperlinked serialization styles together with a URL based versioning scheme make sure to include the request as context to the serializer.
+
+ def get(self, request):
+ queryset = Booking.objects.all()
+ serializer = BookingsSerializer(queryset, many=True, context={'request': request})
+ return Response({'all_bookings': serializer.data})
+
+Doing so will allow any returned URLs to include the appropriate versioning.
+
+## Configuring the versioning scheme
+
+The versioning scheme is defined by the `DEFAULT_VERSIONING_CLASS` settings key.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning'
+ }
+
+Unless it is explicitly set, the value for `DEFAULT_VERSIONING_CLASS` will be `None`. In this case the `request.version` attribute will always return `None`.
+
+You can also set the versioning scheme on an individual view. Typically you won't need to do this, as it makes more sense to have a single versioning scheme used globally. If you do need to do so, use the `versioning_class` attribute.
+
+ class ProfileList(APIView):
+ versioning_class = versioning.QueryParameterVersioning
+
+#### Other versioning settings
+
+The following settings keys are also used to control versioning:
+
+* `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`.
+* `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Defaults to `None`.
+* `VERSION_PARAMETER`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
+
+---
+
+# API Reference
+
+## AcceptHeaderVersioning
+
+This scheme requires the client to specify the version as part of the media type in the `Accept` header. The version is included as a media type parameter, that supplements the main media type.
+
+Here's an example HTTP request using the accept header versioning style.
+
+ GET /bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/json; version=1.0
+
+In the example request above `request.version` attribute would return the string `'1.0'`.
+
+Versioning based on accept headers is [generally considered][klabnik-guidelines] as [best practice][heroku-guidelines], although other styles may be suitable depending on your client requirements.
+
+#### Using accept headers with vendor media types
+
+Strictly speaking the `json` media type is not specified as [including additional parameters][json-parameters]. If you are building a well-specified public API you might consider using a [vendor media type][vendor-media-type]. To do so, configure your renderers to use a JSON based renderer with a custom media type:
+
+ class BookingsAPIRenderer(JSONRenderer):
+ media_type = 'application/vnd.megacorp.bookings+json'
+
+Your client requests would now look like this:
+
+ GET /bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/vnd.megacorp.bookings+json; version=1.0
+
+## URLParameterVersioning
+
+This scheme requires the client to specify the version as part of the URL path.
+
+ GET /v1/bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+Your URL conf must include a pattern that matches the version with a `'version'` keyword argument, so that this information is available to the versioning scheme.
+
+ urlpatterns = [
+ url(
+ r'^(?P{v1,v2})/bookings/$',
+ bookings_list,
+ name='bookings-list'
+ ),
+ url(
+ r'^(?P{v1,v2})/bookings/(?P[0-9]+)/$',
+ bookings_detail,
+ name='bookings-detail'
+ )
+ ]
+
+## NamespaceVersioning
+
+To the client, this scheme is the same as `URLParameterVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments.
+
+ GET /v1/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+With this scheme the `request.version` attribute is determined based on the `namespace` that matches the incoming request path.
+
+In the following example we're giving a set of views two different possible URL prefixes, each under a different namespace:
+
+ # bookings/urls.py
+ urlpatterns = [
+ url(r'^$', bookings_list, name='bookings-list'),
+ url(r'^(?P[0-9]+)/$', bookings_detail, name='bookings-detail')
+ ]
+
+ # urls.py
+ urlpatterns = [
+ url(r'^v1/bookings/', include('bookings.urls', namespace='v1')),
+ url(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
+ ]
+
+Both `URLParameterVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLParameterVersioning` approach might be better suitable for small ad-hoc projects, and the `NaemspaceVersioning` is probably easier to manage for larger projects.
+
+## HostNameVersioning
+
+The hostname versioning scheme requires the client to specify the requested version as part of the hostname in the URL.
+
+For example the following is an HTTP request to the `http://v1.example.com/bookings/` URL:
+
+ GET /bookings/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+
+By default this implementation expects the hostname to match this simple regular expression:
+
+ ^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$
+
+Note that the first group is enclosed in brackets, indicating that this is the matched portion of the hostname.
+
+The `HostNameVersioning` scheme can be awkward to use in debug mode as you will typically be accessing a raw IP address such as `127.0.0.1`. There are various online services which you to [access localhost with a custom subdomain][lvh] which you may find helpful in this case.
+
+Hostname based versioning can be particularly useful if you have requirements to route incoming requests to different servers based on the version, as you can configure different DNS records for different API versions.
+
+## QueryParameterVersioning
+
+This scheme is a simple style that includes the version as a query parameter in the URL. For example:
+
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+---
+
+# Custom versioning schemes
+
+To implement a custom versioning scheme, subclass `BaseVersioning` and override the `.determine_version` method.
+
+## Example
+
+The following example uses a custom `X-API-Version` header to determine the requested version.
+
+ class XAPIVersionScheme(versioning.BaseVersioning):
+ def determine_version(self, request, *args, **kwargs):
+ return request.META.get('HTTP_X_API_VERSION', None)
+
+If your versioning scheme is based on the request URL, you will also want to alter how versioned URLs are determined. In order to do so you should override the `.reverse()` method on the class. See the source code for examples.
+
+[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
+[klabnik-guidelines]: http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned
+[heroku-guidelines]: https://github.com/interagent/http-api-design#version-with-accepts-header
+[json-parameters]: http://tools.ietf.org/html/rfc4627#section-6
+[vendor-media-type]: http://en.wikipedia.org/wiki/Internet_media_type#Vendor_tree
+[lvh]: https://reinteractive.net/posts/199-developing-and-testing-rails-applications-with-subdomains
diff --git a/docs/index.md b/docs/index.md
index 502d352cb..14cf30acb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -175,6 +175,7 @@ The API guide is your complete reference manual to all the functionality provide
* [Throttling][throttling]
* [Filtering][filtering]
* [Pagination][pagination]
+* [Versioning][versioning]
* [Content negotiation][contentnegotiation]
* [Format suffixes][formatsuffixes]
* [Returning URLs][reverse]
@@ -294,6 +295,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[throttling]: api-guide/throttling.md
[filtering]: api-guide/filtering.md
[pagination]: api-guide/pagination.md
+[versioning]: api-guide/versioning.md
[contentnegotiation]: api-guide/content-negotiation.md
[formatsuffixes]: api-guide/format-suffixes.md
[reverse]: api-guide/reverse.md
diff --git a/mkdocs.yml b/mkdocs.yml
index 9513f04f7..c2d6bb524 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -32,6 +32,7 @@ pages:
- ['api-guide/throttling.md', 'API Guide', 'Throttling']
- ['api-guide/filtering.md', 'API Guide', 'Filtering']
- ['api-guide/pagination.md', 'API Guide', 'Pagination']
+ - ['api-guide/versioning.md', 'API Guide', 'Versioning']
- ['api-guide/content-negotiation.md', 'API Guide', 'Content negotiation']
- ['api-guide/format-suffixes.md', 'API Guide', 'Format suffixes']
- ['api-guide/reverse.md', 'API Guide', 'Returning URLs']
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index be41d08d9..238934dbe 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -89,6 +89,11 @@ class PermissionDenied(APIException):
default_detail = _('You do not have permission to perform this action.')
+class NotFound(APIException):
+ status_code = status.HTTP_404_NOT_FOUND
+ default_detail = _('Not found')
+
+
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = _("Method '%s' not allowed.")
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index da3be38dd..877d461be 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -68,6 +68,11 @@ DEFAULTS = {
'SEARCH_PARAM': 'search',
'ORDERING_PARAM': 'ordering',
+ # Versioning
+ 'DEFAULT_VERSION': None,
+ 'ALLOWED_VERSIONS': None,
+ 'VERSION_PARAM': 'version',
+
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 223d0f613..440efd139 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -1,13 +1,20 @@
# coding: utf-8
from __future__ import unicode_literals
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import exceptions
from rest_framework.compat import unicode_http_header
from rest_framework.reverse import _reverse
+from rest_framework.settings import api_settings
from rest_framework.templatetags.rest_framework import replace_query_param
from rest_framework.utils.mediatypes import _MediaType
import re
class BaseVersioning(object):
+ default_version = api_settings.DEFAULT_VERSION
+ allowed_versions = api_settings.ALLOWED_VERSIONS
+ version_param = api_settings.VERSION_PARAM
+
def determine_version(self, request, *args, **kwargs):
msg = '{cls}.determine_version() must be implemented.'
raise NotImplemented(msg.format(
@@ -17,46 +24,10 @@ class BaseVersioning(object):
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
return _reverse(viewname, args, kwargs, request, format, **extra)
-
-class QueryParameterVersioning(BaseVersioning):
- """
- GET /something/?version=0.1 HTTP/1.1
- Host: example.com
- Accept: application/json
- """
- default_version = None
- version_param = 'version'
-
- def determine_version(self, request, *args, **kwargs):
- return request.query_params.get(self.version_param)
-
- def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
- url = super(QueryParameterVersioning, self).reverse(
- viewname, args, kwargs, request, format, **extra
- )
- if request.version is not None:
- return replace_query_param(url, self.version_param, request.version)
- return url
-
-
-class HostNameVersioning(BaseVersioning):
- """
- GET /something/ HTTP/1.1
- Host: v1.example.com
- Accept: application/json
- """
- default_version = None
- hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
-
- def determine_version(self, request, *args, **kwargs):
- hostname, seperator, port = request.get_host().partition(':')
- match = self.hostname_regex.match(hostname)
- if not match:
- return self.default_version
- return match.group(1)
-
- # We don't need to implement `reverse`, as the hostname will already be
- # preserved as part of the REST framework `reverse` implementation.
+ def is_allowed_version(self, version):
+ if not self.allowed_versions:
+ return True
+ return (version == self.default_version) or (version in self.allowed_versions)
class AcceptHeaderVersioning(BaseVersioning):
@@ -65,13 +36,15 @@ class AcceptHeaderVersioning(BaseVersioning):
Host: example.com
Accept: application/json; version=1.0
"""
- default_version = None
- version_param = 'version'
+ invalid_version_message = _("Invalid version in 'Accept' header.")
def determine_version(self, request, *args, **kwargs):
media_type = _MediaType(request.accepted_media_type)
version = media_type.params.get(self.version_param, self.default_version)
- return unicode_http_header(version)
+ version = unicode_http_header(version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotAcceptable(self.invalid_version_message)
+ return version
# We don't need to implement `reverse`, as the versioning is based
# on the `Accept` header, not on the request URL.
@@ -94,11 +67,13 @@ class URLPathVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- default_version = None
- version_param = 'version'
+ invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
- return kwargs.get(self.version_param, self.default_version)
+ version = kwargs.get(self.version_param, self.default_version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
@@ -134,13 +109,16 @@ class NamespaceVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- default_version = None
+ invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
if (resolver_match is None or not resolver_match.namespace):
return self.default_version
- return resolver_match.namespace
+ version = resolver_match.namespace
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
if request.version is not None:
@@ -148,3 +126,49 @@ class NamespaceVersioning(BaseVersioning):
return super(NamespaceVersioning, self).reverse(
viewname, args, kwargs, request, format, **extra
)
+
+
+class HostNameVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+ """
+ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
+ invalid_version_message = _('Invalid version in hostname.')
+
+ def determine_version(self, request, *args, **kwargs):
+ hostname, seperator, port = request.get_host().partition(':')
+ match = self.hostname_regex.match(hostname)
+ if not match:
+ return self.default_version
+ version = match.group(1)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ # We don't need to implement `reverse`, as the hostname will already be
+ # preserved as part of the REST framework `reverse` implementation.
+
+
+class QueryParameterVersioning(BaseVersioning):
+ """
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in query parameter.')
+
+ def determine_version(self, request, *args, **kwargs):
+ version = request.query_params.get(self.version_param)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ url = super(QueryParameterVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+ if request.version is not None:
+ return replace_query_param(url, self.version_param, request.version)
+ return url
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
index eaac5dfb3..c44f727d2 100644
--- a/tests/test_versioning.py
+++ b/tests/test_versioning.py
@@ -1,5 +1,5 @@
from django.conf.urls import include, url
-from rest_framework import versioning
+from rest_framework import status, versioning
from rest_framework.decorators import APIView
from rest_framework.response import Response
from rest_framework.reverse import reverse
@@ -16,6 +16,16 @@ class ReverseView(APIView):
return Response({'url': reverse('another', request=request)})
+class RequestInvalidVersionView(APIView):
+ def determine_version(self, request, *args, **kwargs):
+ scheme = self.versioning_class()
+ scheme.allowed_versions = ('v1', 'v2')
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+
factory = APIRequestFactory()
mock_view = lambda request: None
@@ -150,7 +160,7 @@ class TestURLReversing(APITestCase):
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
- def test_namespace_versioning(self):
+ def test_reverse_namespace_versioning(self):
class FakeResolverMatch:
namespace = 'v1'
@@ -165,3 +175,49 @@ class TestURLReversing(APITestCase):
request = factory.get('/endpoint/')
response = view(request)
assert response.data == {'url': 'http://testserver/another/'}
+
+
+class TestInvalidVersion:
+ def test_invalid_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v3.example.org')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
+
+ def test_invalid_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v3'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
From 5830f7e13817210f5c6d955ad4fedfaa492aa209 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 10:15:36 +0000
Subject: [PATCH 021/121] get_unique_together_validators and
get_unique_for_date_validators
---
rest_framework/serializers.py | 51 ++++++++++++++++++++++++++++-------
1 file changed, 42 insertions(+), 9 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8de22f4b9..55828b03e 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -792,14 +792,33 @@ class ModelSerializer(Serializer):
return instance
def get_validators(self):
+ """
+ Determine the set of validators to use when instantiating serializer.
+ """
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
return validators
- # Determine the default set of validators.
- validators = []
- model_class = self.Meta.model
+ # Otherwise use the default set of validators.
+ return (
+ self.get_unique_together_validators() +
+ self.get_unique_for_date_validators()
+ )
+
+ def get_unique_together_validators(self):
+ """
+ Determine a default set of validators for any unique_together contraints.
+ """
+ model_class_inheritance_tree = (
+ [self.Meta.model] +
+ list(self.Meta.model._meta.parents.keys())
+ )
+
+ # The field names we're passing though here only include fields
+ # which may map onto a model field. Any dotted field name lookups
+ # cannot map to a field, and must be a traversal, so we're not
+ # including those.
field_names = set([
field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source)
@@ -807,7 +826,8 @@ class ModelSerializer(Serializer):
# Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes.
- for parent_class in [model_class] + list(model_class._meta.parents.keys()):
+ validators = []
+ for parent_class in model_class_inheritance_tree:
for unique_together in parent_class._meta.unique_together:
if field_names.issuperset(set(unique_together)):
validator = UniqueTogetherValidator(
@@ -815,13 +835,26 @@ class ModelSerializer(Serializer):
fields=unique_together
)
validators.append(validator)
+ return validators
+
+ def get_unique_for_date_validators(self):
+ """
+ Determine a default set of validators for the following contraints:
+
+ * unique_for_date
+ * unique_for_month
+ * unique_for_year
+ """
+ info = model_meta.get_field_info(self.Meta.model)
+ default_manager = self.Meta.model._default_manager
+ field_names = [field.source for field in self.fields.values()]
+
+ validators = []
- # Add any unique_for_date/unique_for_month/unique_for_year constraints.
- info = model_meta.get_field_info(model_class)
for field_name, field in info.fields_and_pk.items():
if field.unique_for_date and field_name in field_names:
validator = UniqueForDateValidator(
- queryset=model_class._default_manager,
+ queryset=default_manager,
field=field_name,
date_field=field.unique_for_date
)
@@ -829,7 +862,7 @@ class ModelSerializer(Serializer):
if field.unique_for_month and field_name in field_names:
validator = UniqueForMonthValidator(
- queryset=model_class._default_manager,
+ queryset=default_manager,
field=field_name,
date_field=field.unique_for_month
)
@@ -837,7 +870,7 @@ class ModelSerializer(Serializer):
if field.unique_for_year and field_name in field_names:
validator = UniqueForYearValidator(
- queryset=model_class._default_manager,
+ queryset=default_manager,
field=field_name,
date_field=field.unique_for_year
)
From 6d907cde9a90aad76acb00482a1d70550bb95ccd Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 12:18:40 +0000
Subject: [PATCH 022/121] get_field_names, get_default_field_names
---
rest_framework/serializers.py | 102 +++++++++++++++++++++++----------
tests/test_model_serializer.py | 8 +--
2 files changed, 76 insertions(+), 34 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 52ea5b0b6..b391a94eb 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -883,41 +883,14 @@ class ModelSerializer(Serializer):
ret = OrderedDict()
model = getattr(self.Meta, 'model')
- fields = getattr(self.Meta, 'fields', None)
- exclude = getattr(self.Meta, 'exclude', None)
depth = getattr(self.Meta, 'depth', 0)
extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
-
- if fields and not isinstance(fields, (list, tuple)):
- raise TypeError(
- 'The `fields` option must be a list or tuple. Got %s.' %
- type(fields).__name__
- )
-
- if exclude and not isinstance(exclude, (list, tuple)):
- raise TypeError(
- 'The `exclude` option must be a list or tuple. Got %s.' %
- type(exclude).__name__
- )
-
- assert not (fields and exclude), "Cannot set both 'fields' and 'exclude'."
-
extra_kwargs = self._include_additional_options(extra_kwargs)
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
- # Use the default set of field names if none is supplied explicitly.
- if fields is None:
- fields = self._get_default_field_names(declared_fields, info)
- exclude = getattr(self.Meta, 'exclude', None)
- if exclude is not None:
- for field_name in exclude:
- assert field_name in fields, (
- 'The field in the `exclude` option must be a model field. Got %s.' %
- field_name
- )
- fields.remove(field_name)
+ fields = self.get_field_names(declared_fields, info)
# Determine the set of model fields, and the fields that they map to.
# We actually only need this to deal with the slightly awkward case
@@ -1133,7 +1106,72 @@ class ModelSerializer(Serializer):
return extra_kwargs
- def _get_default_field_names(self, declared_fields, model_info):
+ def get_field_names(self, declared_fields, info):
+ """
+ Returns the list of all field names that should be created when
+ instantiating this serializer class. This is based on the default
+ set of fields, but also takes into account the `Meta.fields` or
+ `Meta.exclude` options if they have been specified.
+ """
+ fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
+
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
+
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
+
+ assert not (fields and exclude), (
+ "Cannot set both 'fields' and 'exclude' options on "
+ "serializer {serializer_class}.".format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
+ if fields is not None:
+ # Ensure that all declared fields have also been included in the
+ # `Meta.fields` option.
+ for field_name in declared_fields:
+ assert field_name in fields, (
+ "The field '{field_name}' was declared on serializer "
+ "{serializer_class}, but has not been included in the "
+ "'fields' option.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ return fields
+
+ # Use the default set of field names if `Meta.fields` is not specified.
+ fields = self.get_default_field_names(declared_fields, info)
+
+ if exclude is not None:
+ # If `Meta.exclude` is included, then remove those fields.
+ for field_name in exclude:
+ assert field_name in fields, (
+ "The field '{field_name}' was include on serializer "
+ "{serializer_class} in the 'exclude' option, but does "
+ "not match any model field.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ fields.remove(field_name)
+
+ return fields
+
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
+ """
return (
[model_info.pk.name] +
list(declared_fields.keys()) +
@@ -1160,7 +1198,11 @@ class HyperlinkedModelSerializer(ModelSerializer):
"""
_related_class = HyperlinkedRelatedField
- def _get_default_field_names(self, declared_fields, model_info):
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
+ """
return (
[api_settings.URL_FIELD_NAME] +
list(declared_fields.keys()) +
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index da79164af..5c56c8dbb 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -221,11 +221,11 @@ class TestRegularFieldMappings(TestCase):
model = RegularFieldsModel
fields = ('auto_field',)
- with self.assertRaises(ImproperlyConfigured) as excinfo:
+ with self.assertRaises(AssertionError) as excinfo:
TestSerializer().fields
expected = (
- 'Field `missing` has been declared on serializer '
- '`TestSerializer`, but is missing from `Meta.fields`.'
+ "The field 'missing' was declared on serializer TestSerializer, "
+ "but has not been included in the 'fields' option."
)
assert str(excinfo.exception) == expected
@@ -607,5 +607,5 @@ class TestSerializerMetaClass(TestCase):
exception = result.exception
self.assertEqual(
str(exception),
- "Cannot set both 'fields' and 'exclude'."
+ "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer."
)
From 1a84943a006abffb7e1b3b3ff55441c7a1132fa2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 12:27:50 +0000
Subject: [PATCH 023/121] get_extra_kwargs
---
rest_framework/serializers.py | 22 ++++++++--------------
1 file changed, 8 insertions(+), 14 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index b391a94eb..d4b0926e6 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -884,13 +884,12 @@ class ModelSerializer(Serializer):
ret = OrderedDict()
model = getattr(self.Meta, 'model')
depth = getattr(self.Meta, 'depth', 0)
- extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
- extra_kwargs = self._include_additional_options(extra_kwargs)
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
fields = self.get_field_names(declared_fields, info)
+ extra_kwargs = self.get_extra_kwargs()
# Determine the set of model fields, and the fields that they map to.
# We actually only need this to deal with the slightly awkward case
@@ -1024,17 +1023,6 @@ class ModelSerializer(Serializer):
(field_name, model.__class__.__name__)
)
- # Check that any fields declared on the class are
- # also explicitly included in `Meta.fields`.
- missing_fields = set(declared_fields.keys()) - set(fields)
- if missing_fields:
- missing_field = list(missing_fields)[0]
- raise ImproperlyConfigured(
- 'Field `%s` has been declared on serializer `%s`, but '
- 'is missing from `Meta.fields`.' %
- (missing_field, self.__class__.__name__)
- )
-
# Populate any kwargs defined in `Meta.extra_kwargs`
extras = extra_kwargs.get(field_name, {})
if extras.get('read_only', False):
@@ -1058,7 +1046,13 @@ class ModelSerializer(Serializer):
return ret
- def _include_additional_options(self, extra_kwargs):
+ def get_extra_kwargs(self):
+ """
+ Return a dictionary mapping field names to a dictionary of
+ additional keyword arguments.
+ """
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+
read_only_fields = getattr(self.Meta, 'read_only_fields', None)
if read_only_fields is not None:
for field_name in read_only_fields:
From caa13181244ce3c074f647510bb38d7b0c8b4c70 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 13:13:20 +0000
Subject: [PATCH 024/121] get_uniqueness_field_options first pass
---
rest_framework/serializers.py | 174 +++++++++++++++++++---------------
1 file changed, 96 insertions(+), 78 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d4b0926e6..5e9cbe361 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -888,89 +888,19 @@ class ModelSerializer(Serializer):
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
- fields = self.get_field_names(declared_fields, info)
+ field_names = self.get_field_names(declared_fields, info)
extra_kwargs = self.get_extra_kwargs()
- # Determine the set of model fields, and the fields that they map to.
- # We actually only need this to deal with the slightly awkward case
- # of supporting `unique_for_date`/`unique_for_month`/`unique_for_year`.
- model_field_mapping = {}
- for field_name in fields:
- if field_name in declared_fields:
- field = declared_fields[field_name]
- source = field.source or field_name
+ model_fields = self.get_model_fields(field_names, declared_fields, extra_kwargs)
+ uniqueness_extra_kwargs, hidden_fields = self.get_uniqueness_field_options(field_names, model_fields)
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
else:
- try:
- source = extra_kwargs[field_name]['source']
- except KeyError:
- source = field_name
- # Model fields will always have a simple source mapping,
- # they can't be nested attribute lookups.
- if '.' not in source and source != '*':
- model_field_mapping[source] = field_name
-
- # Determine if we need any additional `HiddenField` or extra keyword
- # arguments to deal with `unique_for` dates that are required to
- # be in the input data in order to validate it.
- hidden_fields = {}
- unique_constraint_names = set()
-
- for model_field_name, field_name in model_field_mapping.items():
- try:
- model_field = model._meta.get_field(model_field_name)
- except FieldDoesNotExist:
- continue
-
- # Include each of the `unique_for_*` field names.
- unique_constraint_names |= set([
- model_field.unique_for_date,
- model_field.unique_for_month,
- model_field.unique_for_year
- ])
-
- unique_constraint_names -= set([None])
-
- # Include each of the `unique_together` field names,
- # so long as all the field names are included on the serializer.
- for parent_class in [model] + list(model._meta.parents.keys()):
- for unique_together_list in parent_class._meta.unique_together:
- if set(fields).issuperset(set(unique_together_list)):
- unique_constraint_names |= set(unique_together_list)
-
- # Now we have all the field names that have uniqueness constraints
- # applied, we can add the extra 'required=...' or 'default=...'
- # arguments that are appropriate to these fields, or add a `HiddenField` for it.
- for unique_constraint_name in unique_constraint_names:
- # Get the model field that is referred too.
- unique_constraint_field = model._meta.get_field(unique_constraint_name)
-
- if getattr(unique_constraint_field, 'auto_now_add', None):
- default = CreateOnlyDefault(timezone.now)
- elif getattr(unique_constraint_field, 'auto_now', None):
- default = timezone.now
- elif unique_constraint_field.has_default():
- default = unique_constraint_field.default
- else:
- default = empty
-
- if unique_constraint_name in model_field_mapping:
- # The corresponding field is present in the serializer
- if unique_constraint_name not in extra_kwargs:
- extra_kwargs[unique_constraint_name] = {}
- if default is empty:
- if 'required' not in extra_kwargs[unique_constraint_name]:
- extra_kwargs[unique_constraint_name]['required'] = True
- else:
- if 'default' not in extra_kwargs[unique_constraint_name]:
- extra_kwargs[unique_constraint_name]['default'] = default
- elif default is not empty:
- # The corresponding field is not present in the,
- # serializer. We have a default to use for it, so
- # add in a hidden field that populates it.
- hidden_fields[unique_constraint_name] = HiddenField(default=default)
+ extra_kwargs[key] = value
# Now determine the fields that should be included on the serializer.
- for field_name in fields:
+ for field_name in field_names:
if field_name in declared_fields:
# Field is explicitly declared on the class, use that.
ret[field_name] = declared_fields[field_name]
@@ -1046,6 +976,94 @@ class ModelSerializer(Serializer):
return ret
+ def get_model_fields(self, field_names, declared_fields, extra_kwargs):
+ # Returns all the model fields that are being mapped to by fields
+ # on the serializer class.
+ # Returned as a dict of 'model field name' -> 'model field'
+ model = getattr(self.Meta, 'model')
+ model_fields = {}
+
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # If the field is declared on the serializer
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
+
+ if '.' in source or source == '*':
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ continue
+
+ try:
+ model_fields[source] = model._meta.get_field(source)
+ except FieldDoesNotExist:
+ pass
+
+ return model_fields
+
+ def get_uniqueness_field_options(self, field_names, model_fields):
+ model = getattr(self.Meta, 'model')
+
+ # Determine if we need any additional `HiddenField` or extra keyword
+ # arguments to deal with `unique_for` dates that are required to
+ # be in the input data in order to validate it.
+ unique_constraint_names = set()
+
+ for model_field in model_fields.values():
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
+ model_field.unique_for_date,
+ model_field.unique_for_month,
+ model_field.unique_for_year
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(field_names).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ hidden_fields = {}
+ extra_kwargs = {}
+
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is referred too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
+
+ if unique_constraint_name in model_fields:
+ # The corresponding field is present in the serializer
+ if default is empty:
+ extra_kwargs[unique_constraint_name] = {'required': True}
+ else:
+ extra_kwargs[unique_constraint_name] = {'default': default}
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
+
+ return extra_kwargs, hidden_fields
+
def get_extra_kwargs(self):
"""
Return a dictionary mapping field names to a dictionary of
From 4a112fc3a616238b7995b3a442ae236116364ceb Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 14:51:45 +0000
Subject: [PATCH 025/121] Clean up
---
rest_framework/serializers.py | 63 +++++++++++++++++++++++------------
1 file changed, 42 insertions(+), 21 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 5e9cbe361..093b0eb51 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -722,6 +722,8 @@ class ModelSerializer(Serializer):
})
_related_class = PrimaryKeyRelatedField
+ # Default `create` and `update` behavior...
+
def create(self, validated_data):
"""
We have a bit of extra checking around this in order to provide
@@ -791,6 +793,8 @@ class ModelSerializer(Serializer):
return instance
+ # Determine the validators to apply...
+
def get_validators(self):
"""
Determine the set of validators to use when instantiating serializer.
@@ -878,28 +882,26 @@ class ModelSerializer(Serializer):
return validators
+ # Determine the fields to apply...
+
def get_fields(self):
declared_fields = copy.deepcopy(self._declared_fields)
-
- ret = OrderedDict()
model = getattr(self.Meta, 'model')
depth = getattr(self.Meta, 'depth', 0)
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
-
field_names = self.get_field_names(declared_fields, info)
- extra_kwargs = self.get_extra_kwargs()
- model_fields = self.get_model_fields(field_names, declared_fields, extra_kwargs)
- uniqueness_extra_kwargs, hidden_fields = self.get_uniqueness_field_options(field_names, model_fields)
- for key, value in uniqueness_extra_kwargs.items():
- if key in extra_kwargs:
- extra_kwargs[key].update(value)
- else:
- extra_kwargs[key] = value
+ # Determine any extra field arguments and hidden fields that
+ # should be included
+ extra_kwargs = self.get_extra_kwargs()
+ extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
+ field_names, declared_fields, extra_kwargs
+ )
# Now determine the fields that should be included on the serializer.
+ ret = OrderedDict()
for field_name in field_names:
if field_name in declared_fields:
# Field is explicitly declared on the class, use that.
@@ -971,15 +973,17 @@ class ModelSerializer(Serializer):
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
- for field_name, field in hidden_fields.items():
- ret[field_name] = field
+ ret.update(hidden_fields)
return ret
- def get_model_fields(self, field_names, declared_fields, extra_kwargs):
- # Returns all the model fields that are being mapped to by fields
- # on the serializer class.
- # Returned as a dict of 'model field name' -> 'model field'
+ def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
+ """
+ Returns all the model fields that are being mapped to by fields
+ on the serializer class.
+ Returned as a dict of 'model field name' -> 'model field'.
+ Used internally by `get_uniqueness_field_options`.
+ """
model = getattr(self.Meta, 'model')
model_fields = {}
@@ -1006,8 +1010,18 @@ class ModelSerializer(Serializer):
return model_fields
- def get_uniqueness_field_options(self, field_names, model_fields):
+ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
+ """
+ Return any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('dict of updated extra kwargs', 'mapping of hidden fields')
+ """
model = getattr(self.Meta, 'model')
+ model_fields = self._get_model_fields(
+ field_names, declared_fields, extra_kwargs
+ )
# Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to
@@ -1035,7 +1049,7 @@ class ModelSerializer(Serializer):
# applied, we can add the extra 'required=...' or 'default=...'
# arguments that are appropriate to these fields, or add a `HiddenField` for it.
hidden_fields = {}
- extra_kwargs = {}
+ uniqueness_extra_kwargs = {}
for unique_constraint_name in unique_constraint_names:
# Get the model field that is referred too.
@@ -1053,15 +1067,22 @@ class ModelSerializer(Serializer):
if unique_constraint_name in model_fields:
# The corresponding field is present in the serializer
if default is empty:
- extra_kwargs[unique_constraint_name] = {'required': True}
+ uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
else:
- extra_kwargs[unique_constraint_name] = {'default': default}
+ uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
elif default is not empty:
# The corresponding field is not present in the,
# serializer. We have a default to use for it, so
# add in a hidden field that populates it.
hidden_fields[unique_constraint_name] = HiddenField(default=default)
+ # Update `extra_kwargs` with any new options.
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
+ else:
+ extra_kwargs[key] = value
+
return extra_kwargs, hidden_fields
def get_extra_kwargs(self):
From f72928ea982cfe2127288dd6dc52f8006638b0c3 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 15:09:57 +0000
Subject: [PATCH 026/121] build_field, build_final_kwargs
---
rest_framework/serializers.py | 135 +++++++++++++++++++---------------
1 file changed, 76 insertions(+), 59 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 1f76c4c19..80ad10f0a 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -908,75 +908,92 @@ class ModelSerializer(Serializer):
ret[field_name] = declared_fields[field_name]
continue
- elif field_name in info.fields_and_pk:
- # Create regular model fields.
- model_field = info.fields_and_pk[field_name]
- field_cls = self._field_mapping[model_field]
- kwargs = get_field_kwargs(field_name, model_field)
- if 'choices' in kwargs:
- # Fields with choices get coerced into `ChoiceField`
- # instead of using their regular typed field.
- field_cls = ChoiceField
- if not issubclass(field_cls, ModelField):
- # `model_field` is only valid for the fallback case of
- # `ModelField`, which is used when no other typed field
- # matched to the model field.
- kwargs.pop('model_field', None)
- if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
- # `allow_blank` is only valid for textual fields.
- kwargs.pop('allow_blank', None)
-
- elif field_name in info.relations:
- # Create forward and reverse relationships.
- relation_info = info.relations[field_name]
- if depth:
- field_cls = self._get_nested_class(depth, relation_info)
- kwargs = get_nested_relation_kwargs(relation_info)
- else:
- field_cls = self._related_class
- kwargs = get_relation_kwargs(field_name, relation_info)
- # `view_name` is only valid for hyperlinked relationships.
- if not issubclass(field_cls, HyperlinkedRelatedField):
- kwargs.pop('view_name', None)
-
- elif hasattr(model, field_name):
- # Create a read only field for model methods and properties.
- field_cls = ReadOnlyField
- kwargs = {}
-
- elif field_name == api_settings.URL_FIELD_NAME:
- # Create the URL field.
- field_cls = HyperlinkedIdentityField
- kwargs = get_url_kwargs(model)
-
- else:
- raise ImproperlyConfigured(
- 'Field name `%s` is not valid for model `%s`.' %
- (field_name, model.__class__.__name__)
- )
+ # Determine the serializer field class and keyword arguments.
+ field_cls, kwargs = self.build_field(field_name, info, model, depth)
# Populate any kwargs defined in `Meta.extra_kwargs`
- extras = extra_kwargs.get(field_name, {})
- if extras.get('read_only', False):
- for attr in [
- 'required', 'default', 'allow_blank', 'allow_null',
- 'min_length', 'max_length', 'min_value', 'max_value',
- 'validators', 'queryset'
- ]:
- kwargs.pop(attr, None)
-
- if extras.get('default') and kwargs.get('required') is False:
- kwargs.pop('required')
-
- kwargs.update(extras)
+ kwargs = self.build_final_kwargs(kwargs, extra_kwargs, field_name)
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
+ # Add in any hidden fields.
ret.update(hidden_fields)
return ret
+ def build_field(self, field_name, info, model, depth):
+ if field_name in info.fields_and_pk:
+ # Create regular model fields.
+ model_field = info.fields_and_pk[field_name]
+ field_cls = self._field_mapping[model_field]
+ kwargs = get_field_kwargs(field_name, model_field)
+ if 'choices' in kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_cls = ChoiceField
+ if not issubclass(field_cls, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ kwargs.pop('model_field', None)
+ if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
+ # `allow_blank` is only valid for textual fields.
+ kwargs.pop('allow_blank', None)
+
+ elif field_name in info.relations:
+ # Create forward and reverse relationships.
+ relation_info = info.relations[field_name]
+ if depth:
+ field_cls = self._get_nested_class(depth, relation_info)
+ kwargs = get_nested_relation_kwargs(relation_info)
+ else:
+ field_cls = self._related_class
+ kwargs = get_relation_kwargs(field_name, relation_info)
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_cls, HyperlinkedRelatedField):
+ kwargs.pop('view_name', None)
+
+ elif hasattr(model, field_name):
+ # Create a read only field for model methods and properties.
+ field_cls = ReadOnlyField
+ kwargs = {}
+
+ elif field_name == api_settings.URL_FIELD_NAME:
+ # Create the URL field.
+ field_cls = HyperlinkedIdentityField
+ kwargs = get_url_kwargs(model)
+
+ else:
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model.__class__.__name__)
+ )
+
+ return field_cls, kwargs
+
+ def build_final_kwargs(self, kwargs, extra_kwargs, field_name):
+ """
+ Include an 'extra_kwargs' that have been included for this field,
+ possibly removing any incompatible existing keyword arguments.
+ """
+ extras = extra_kwargs.get(field_name, {})
+
+ if extras.get('read_only', False):
+ for attr in [
+ 'required', 'default', 'allow_blank', 'allow_null',
+ 'min_length', 'max_length', 'min_value', 'max_value',
+ 'validators', 'queryset'
+ ]:
+ kwargs.pop(attr, None)
+
+ if extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
+ kwargs.update(extras)
+
+ return kwargs
+
def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
"""
Returns all the model fields that are being mapped to by fields
From 75e81b82545704bac8afdf3270ba9f6c8da09c27 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 15:35:52 +0000
Subject: [PATCH 027/121] build_*_field methods
---
rest_framework/serializers.py | 785 ++++++++++++++++++----------------
1 file changed, 424 insertions(+), 361 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 80ad10f0a..a983d3fc7 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -696,7 +696,7 @@ class ModelSerializer(Serializer):
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a `Serializer` class.
"""
- _field_mapping = ClassLookupDict({
+ serializer_field_mapping = {
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
@@ -719,8 +719,8 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
- })
- _related_class = PrimaryKeyRelatedField
+ }
+ serializer_related_class = PrimaryKeyRelatedField
# Default `create` and `update` behavior...
@@ -793,6 +793,417 @@ class ModelSerializer(Serializer):
return instance
+ # Determine the fields to apply...
+
+ def get_fields(self):
+ """
+ Return the dict of field names -> field instances that should be
+ used for `self.fields` when instantiating the serializer.
+ """
+ declared_fields = copy.deepcopy(self._declared_fields)
+ model = getattr(self.Meta, 'model')
+ depth = getattr(self.Meta, 'depth', 0)
+
+ # Retrieve metadata about fields & relationships on the model class.
+ info = model_meta.get_field_info(model)
+ field_names = self.get_field_names(declared_fields, info)
+
+ # Determine any extra field arguments and hidden fields that
+ # should be included
+ extra_kwargs = self.get_extra_kwargs()
+ extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
+ field_names, declared_fields, extra_kwargs
+ )
+
+ # Now determine the fields that should be included on the serializer.
+ ret = OrderedDict()
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # Field is explicitly declared on the class, use that.
+ ret[field_name] = declared_fields[field_name]
+ continue
+
+ # Determine the serializer field class and keyword arguments.
+ field_cls, kwargs = self.build_field(field_name, info, model, depth)
+
+ # Populate any kwargs defined in `Meta.extra_kwargs`
+ kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name)
+
+ # Create the serializer field.
+ ret[field_name] = field_cls(**kwargs)
+
+ # Add in any hidden fields.
+ ret.update(hidden_fields)
+
+ return ret
+
+ # Methods for determining the set of field names to include...
+
+ def get_field_names(self, declared_fields, info):
+ """
+ Returns the list of all field names that should be created when
+ instantiating this serializer class. This is based on the default
+ set of fields, but also takes into account the `Meta.fields` or
+ `Meta.exclude` options if they have been specified.
+ """
+ fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
+
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
+
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
+
+ assert not (fields and exclude), (
+ "Cannot set both 'fields' and 'exclude' options on "
+ "serializer {serializer_class}.".format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
+ if fields is not None:
+ # Ensure that all declared fields have also been included in the
+ # `Meta.fields` option.
+ for field_name in declared_fields:
+ assert field_name in fields, (
+ "The field '{field_name}' was declared on serializer "
+ "{serializer_class}, but has not been included in the "
+ "'fields' option.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ return fields
+
+ # Use the default set of field names if `Meta.fields` is not specified.
+ fields = self.get_default_field_names(declared_fields, info)
+
+ if exclude is not None:
+ # If `Meta.exclude` is included, then remove those fields.
+ for field_name in exclude:
+ assert field_name in fields, (
+ "The field '{field_name}' was include on serializer "
+ "{serializer_class} in the 'exclude' option, but does "
+ "not match any model field.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ fields.remove(field_name)
+
+ return fields
+
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
+ """
+ return (
+ [model_info.pk.name] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
+
+ # Methods for constructing serializer fields...
+
+ def build_field(self, field_name, info, model, nested_depth):
+ """
+ Return a two tuple of (cls, kwargs) to build a serializer field with.
+ """
+ if field_name in info.fields_and_pk:
+ return self.build_standard_field(field_name, info, model)
+
+ elif field_name in info.relations:
+ if not nested_depth:
+ return self.build_relational_field(field_name, info, model)
+ else:
+ return self.build_nested_field(field_name, info, model, nested_depth)
+
+ elif hasattr(model, field_name):
+ return self.build_property_field(field_name, info, model)
+
+ elif field_name == api_settings.URL_FIELD_NAME:
+ return self.build_url_field(field_name, info, model)
+
+ return self.build_unknown_field(field_name, info, model)
+
+ def build_standard_field(self, field_name, info, model):
+ """
+ Create regular model fields.
+ """
+ field_mapping = ClassLookupDict(self.serializer_field_mapping)
+ model_field = info.fields_and_pk[field_name]
+
+ field_cls = field_mapping[model_field]
+ kwargs = get_field_kwargs(field_name, model_field)
+
+ if 'choices' in kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_cls = ChoiceField
+ if not issubclass(field_cls, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ kwargs.pop('model_field', None)
+ if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
+ # `allow_blank` is only valid for textual fields.
+ kwargs.pop('allow_blank', None)
+
+ return field_cls, kwargs
+
+ def build_relational_field(self, field_name, info, model):
+ """
+ Create fields for forward and reverse relationships.
+ """
+ relation_info = info.relations[field_name]
+
+ field_cls = self.serializer_related_class
+ kwargs = get_relation_kwargs(field_name, relation_info)
+
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_cls, HyperlinkedRelatedField):
+ kwargs.pop('view_name', None)
+
+ return field_cls, kwargs
+
+ def build_nested_field(self, field_name, info, model, nested_depth):
+ """
+ Create nested fields for forward and reverse relationships.
+ """
+ relation_info = info.relations[field_name]
+
+ class NestedSerializer(ModelSerializer):
+ class Meta:
+ model = relation_info.related
+ depth = nested_depth - 1
+
+ field_cls = NestedSerializer
+ kwargs = get_nested_relation_kwargs(relation_info)
+
+ return field_cls, kwargs
+
+ def build_property_field(self, field_name, info, model):
+ """
+ Create a read only field for model methods and properties.
+ """
+ field_cls = ReadOnlyField
+ kwargs = {}
+
+ return field_cls, kwargs
+
+ def build_url_field(self, field_name, info, model):
+ """
+ Create a field representing the object's own URL.
+ """
+ field_cls = HyperlinkedIdentityField
+ kwargs = get_url_kwargs(model)
+
+ return field_cls, kwargs
+
+ def build_unknown_field(self, field_name, info, model):
+ """
+ Raise an error on any unknown fields.
+ """
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model.__class__.__name__)
+ )
+
+ def build_field_kwargs(self, kwargs, extra_kwargs, field_name):
+ """
+ Include an 'extra_kwargs' that have been included for this field,
+ possibly removing any incompatible existing keyword arguments.
+ """
+ extras = extra_kwargs.get(field_name, {})
+
+ if extras.get('read_only', False):
+ for attr in [
+ 'required', 'default', 'allow_blank', 'allow_null',
+ 'min_length', 'max_length', 'min_value', 'max_value',
+ 'validators', 'queryset'
+ ]:
+ kwargs.pop(attr, None)
+
+ if extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
+ kwargs.update(extras)
+
+ return kwargs
+
+ # Methods for determining additional keyword arguments to apply...
+
+ def get_extra_kwargs(self):
+ """
+ Return a dictionary mapping field names to a dictionary of
+ additional keyword arguments.
+ """
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+
+ read_only_fields = getattr(self.Meta, 'read_only_fields', None)
+ if read_only_fields is not None:
+ for field_name in read_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['read_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ # These are all pending deprecation.
+ write_only_fields = getattr(self.Meta, 'write_only_fields', None)
+ if write_only_fields is not None:
+ warnings.warn(
+ "The `Meta.write_only_fields` option is pending deprecation. "
+ "Use `Meta.extra_kwargs={: {'write_only': True}}` instead.",
+ PendingDeprecationWarning,
+ stacklevel=3
+ )
+ for field_name in write_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['write_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ view_name = getattr(self.Meta, 'view_name', None)
+ if view_name is not None:
+ warnings.warn(
+ "The `Meta.view_name` option is pending deprecation. "
+ "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
+ PendingDeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['view_name'] = view_name
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
+
+ lookup_field = getattr(self.Meta, 'lookup_field', None)
+ if lookup_field is not None:
+ warnings.warn(
+ "The `Meta.lookup_field` option is pending deprecation. "
+ "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
+ PendingDeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['lookup_field'] = lookup_field
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
+
+ return extra_kwargs
+
+ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
+ """
+ Return any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('dict of updated extra kwargs', 'mapping of hidden fields')
+ """
+ model = getattr(self.Meta, 'model')
+ model_fields = self._get_model_fields(
+ field_names, declared_fields, extra_kwargs
+ )
+
+ # Determine if we need any additional `HiddenField` or extra keyword
+ # arguments to deal with `unique_for` dates that are required to
+ # be in the input data in order to validate it.
+ unique_constraint_names = set()
+
+ for model_field in model_fields.values():
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
+ model_field.unique_for_date,
+ model_field.unique_for_month,
+ model_field.unique_for_year
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(field_names).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ hidden_fields = {}
+ uniqueness_extra_kwargs = {}
+
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is referred too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
+
+ if unique_constraint_name in model_fields:
+ # The corresponding field is present in the serializer
+ if default is empty:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
+ else:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
+
+ # Update `extra_kwargs` with any new options.
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
+ else:
+ extra_kwargs[key] = value
+
+ return extra_kwargs, hidden_fields
+
+ def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
+ """
+ Returns all the model fields that are being mapped to by fields
+ on the serializer class.
+ Returned as a dict of 'model field name' -> 'model field'.
+ Used internally by `get_uniqueness_field_options`.
+ """
+ model = getattr(self.Meta, 'model')
+ model_fields = {}
+
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # If the field is declared on the serializer
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
+
+ if '.' in source or source == '*':
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ continue
+
+ try:
+ model_fields[source] = model._meta.get_field(source)
+ except FieldDoesNotExist:
+ pass
+
+ return model_fields
+
# Determine the validators to apply...
def get_validators(self):
@@ -882,361 +1293,6 @@ class ModelSerializer(Serializer):
return validators
- # Determine the fields to apply...
-
- def get_fields(self):
- declared_fields = copy.deepcopy(self._declared_fields)
- model = getattr(self.Meta, 'model')
- depth = getattr(self.Meta, 'depth', 0)
-
- # Retrieve metadata about fields & relationships on the model class.
- info = model_meta.get_field_info(model)
- field_names = self.get_field_names(declared_fields, info)
-
- # Determine any extra field arguments and hidden fields that
- # should be included
- extra_kwargs = self.get_extra_kwargs()
- extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
- field_names, declared_fields, extra_kwargs
- )
-
- # Now determine the fields that should be included on the serializer.
- ret = OrderedDict()
- for field_name in field_names:
- if field_name in declared_fields:
- # Field is explicitly declared on the class, use that.
- ret[field_name] = declared_fields[field_name]
- continue
-
- # Determine the serializer field class and keyword arguments.
- field_cls, kwargs = self.build_field(field_name, info, model, depth)
-
- # Populate any kwargs defined in `Meta.extra_kwargs`
- kwargs = self.build_final_kwargs(kwargs, extra_kwargs, field_name)
-
- # Create the serializer field.
- ret[field_name] = field_cls(**kwargs)
-
- # Add in any hidden fields.
- ret.update(hidden_fields)
-
- return ret
-
- def build_field(self, field_name, info, model, depth):
- if field_name in info.fields_and_pk:
- # Create regular model fields.
- model_field = info.fields_and_pk[field_name]
- field_cls = self._field_mapping[model_field]
- kwargs = get_field_kwargs(field_name, model_field)
- if 'choices' in kwargs:
- # Fields with choices get coerced into `ChoiceField`
- # instead of using their regular typed field.
- field_cls = ChoiceField
- if not issubclass(field_cls, ModelField):
- # `model_field` is only valid for the fallback case of
- # `ModelField`, which is used when no other typed field
- # matched to the model field.
- kwargs.pop('model_field', None)
- if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
- # `allow_blank` is only valid for textual fields.
- kwargs.pop('allow_blank', None)
-
- elif field_name in info.relations:
- # Create forward and reverse relationships.
- relation_info = info.relations[field_name]
- if depth:
- field_cls = self._get_nested_class(depth, relation_info)
- kwargs = get_nested_relation_kwargs(relation_info)
- else:
- field_cls = self._related_class
- kwargs = get_relation_kwargs(field_name, relation_info)
- # `view_name` is only valid for hyperlinked relationships.
- if not issubclass(field_cls, HyperlinkedRelatedField):
- kwargs.pop('view_name', None)
-
- elif hasattr(model, field_name):
- # Create a read only field for model methods and properties.
- field_cls = ReadOnlyField
- kwargs = {}
-
- elif field_name == api_settings.URL_FIELD_NAME:
- # Create the URL field.
- field_cls = HyperlinkedIdentityField
- kwargs = get_url_kwargs(model)
-
- else:
- raise ImproperlyConfigured(
- 'Field name `%s` is not valid for model `%s`.' %
- (field_name, model.__class__.__name__)
- )
-
- return field_cls, kwargs
-
- def build_final_kwargs(self, kwargs, extra_kwargs, field_name):
- """
- Include an 'extra_kwargs' that have been included for this field,
- possibly removing any incompatible existing keyword arguments.
- """
- extras = extra_kwargs.get(field_name, {})
-
- if extras.get('read_only', False):
- for attr in [
- 'required', 'default', 'allow_blank', 'allow_null',
- 'min_length', 'max_length', 'min_value', 'max_value',
- 'validators', 'queryset'
- ]:
- kwargs.pop(attr, None)
-
- if extras.get('default') and kwargs.get('required') is False:
- kwargs.pop('required')
-
- kwargs.update(extras)
-
- return kwargs
-
- def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
- """
- Returns all the model fields that are being mapped to by fields
- on the serializer class.
- Returned as a dict of 'model field name' -> 'model field'.
- Used internally by `get_uniqueness_field_options`.
- """
- model = getattr(self.Meta, 'model')
- model_fields = {}
-
- for field_name in field_names:
- if field_name in declared_fields:
- # If the field is declared on the serializer
- field = declared_fields[field_name]
- source = field.source or field_name
- else:
- try:
- source = extra_kwargs[field_name]['source']
- except KeyError:
- source = field_name
-
- if '.' in source or source == '*':
- # Model fields will always have a simple source mapping,
- # they can't be nested attribute lookups.
- continue
-
- try:
- model_fields[source] = model._meta.get_field(source)
- except FieldDoesNotExist:
- pass
-
- return model_fields
-
- def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
- """
- Return any additional field options that need to be included as a
- result of uniqueness constraints on the model. This is returned as
- a two-tuple of:
-
- ('dict of updated extra kwargs', 'mapping of hidden fields')
- """
- model = getattr(self.Meta, 'model')
- model_fields = self._get_model_fields(
- field_names, declared_fields, extra_kwargs
- )
-
- # Determine if we need any additional `HiddenField` or extra keyword
- # arguments to deal with `unique_for` dates that are required to
- # be in the input data in order to validate it.
- unique_constraint_names = set()
-
- for model_field in model_fields.values():
- # Include each of the `unique_for_*` field names.
- unique_constraint_names |= set([
- model_field.unique_for_date,
- model_field.unique_for_month,
- model_field.unique_for_year
- ])
-
- unique_constraint_names -= set([None])
-
- # Include each of the `unique_together` field names,
- # so long as all the field names are included on the serializer.
- for parent_class in [model] + list(model._meta.parents.keys()):
- for unique_together_list in parent_class._meta.unique_together:
- if set(field_names).issuperset(set(unique_together_list)):
- unique_constraint_names |= set(unique_together_list)
-
- # Now we have all the field names that have uniqueness constraints
- # applied, we can add the extra 'required=...' or 'default=...'
- # arguments that are appropriate to these fields, or add a `HiddenField` for it.
- hidden_fields = {}
- uniqueness_extra_kwargs = {}
-
- for unique_constraint_name in unique_constraint_names:
- # Get the model field that is referred too.
- unique_constraint_field = model._meta.get_field(unique_constraint_name)
-
- if getattr(unique_constraint_field, 'auto_now_add', None):
- default = CreateOnlyDefault(timezone.now)
- elif getattr(unique_constraint_field, 'auto_now', None):
- default = timezone.now
- elif unique_constraint_field.has_default():
- default = unique_constraint_field.default
- else:
- default = empty
-
- if unique_constraint_name in model_fields:
- # The corresponding field is present in the serializer
- if default is empty:
- uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
- else:
- uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
- elif default is not empty:
- # The corresponding field is not present in the,
- # serializer. We have a default to use for it, so
- # add in a hidden field that populates it.
- hidden_fields[unique_constraint_name] = HiddenField(default=default)
-
- # Update `extra_kwargs` with any new options.
- for key, value in uniqueness_extra_kwargs.items():
- if key in extra_kwargs:
- extra_kwargs[key].update(value)
- else:
- extra_kwargs[key] = value
-
- return extra_kwargs, hidden_fields
-
- def get_extra_kwargs(self):
- """
- Return a dictionary mapping field names to a dictionary of
- additional keyword arguments.
- """
- extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
-
- read_only_fields = getattr(self.Meta, 'read_only_fields', None)
- if read_only_fields is not None:
- for field_name in read_only_fields:
- kwargs = extra_kwargs.get(field_name, {})
- kwargs['read_only'] = True
- extra_kwargs[field_name] = kwargs
-
- # These are all pending deprecation.
- write_only_fields = getattr(self.Meta, 'write_only_fields', None)
- if write_only_fields is not None:
- warnings.warn(
- "The `Meta.write_only_fields` option is pending deprecation. "
- "Use `Meta.extra_kwargs={: {'write_only': True}}` instead.",
- PendingDeprecationWarning,
- stacklevel=3
- )
- for field_name in write_only_fields:
- kwargs = extra_kwargs.get(field_name, {})
- kwargs['write_only'] = True
- extra_kwargs[field_name] = kwargs
-
- view_name = getattr(self.Meta, 'view_name', None)
- if view_name is not None:
- warnings.warn(
- "The `Meta.view_name` option is pending deprecation. "
- "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
- PendingDeprecationWarning,
- stacklevel=3
- )
- kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
- kwargs['view_name'] = view_name
- extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
-
- lookup_field = getattr(self.Meta, 'lookup_field', None)
- if lookup_field is not None:
- warnings.warn(
- "The `Meta.lookup_field` option is pending deprecation. "
- "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
- PendingDeprecationWarning,
- stacklevel=3
- )
- kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
- kwargs['lookup_field'] = lookup_field
- extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
-
- return extra_kwargs
-
- def get_field_names(self, declared_fields, info):
- """
- Returns the list of all field names that should be created when
- instantiating this serializer class. This is based on the default
- set of fields, but also takes into account the `Meta.fields` or
- `Meta.exclude` options if they have been specified.
- """
- fields = getattr(self.Meta, 'fields', None)
- exclude = getattr(self.Meta, 'exclude', None)
-
- if fields and not isinstance(fields, (list, tuple)):
- raise TypeError(
- 'The `fields` option must be a list or tuple. Got %s.' %
- type(fields).__name__
- )
-
- if exclude and not isinstance(exclude, (list, tuple)):
- raise TypeError(
- 'The `exclude` option must be a list or tuple. Got %s.' %
- type(exclude).__name__
- )
-
- assert not (fields and exclude), (
- "Cannot set both 'fields' and 'exclude' options on "
- "serializer {serializer_class}.".format(
- serializer_class=self.__class__.__name__
- )
- )
-
- if fields is not None:
- # Ensure that all declared fields have also been included in the
- # `Meta.fields` option.
- for field_name in declared_fields:
- assert field_name in fields, (
- "The field '{field_name}' was declared on serializer "
- "{serializer_class}, but has not been included in the "
- "'fields' option.".format(
- field_name=field_name,
- serializer_class=self.__class__.__name__
- )
- )
- return fields
-
- # Use the default set of field names if `Meta.fields` is not specified.
- fields = self.get_default_field_names(declared_fields, info)
-
- if exclude is not None:
- # If `Meta.exclude` is included, then remove those fields.
- for field_name in exclude:
- assert field_name in fields, (
- "The field '{field_name}' was include on serializer "
- "{serializer_class} in the 'exclude' option, but does "
- "not match any model field.".format(
- field_name=field_name,
- serializer_class=self.__class__.__name__
- )
- )
- fields.remove(field_name)
-
- return fields
-
- def get_default_field_names(self, declared_fields, model_info):
- """
- Return the default list of field names that will be used if the
- `Meta.fields` option is not specified.
- """
- return (
- [model_info.pk.name] +
- list(declared_fields.keys()) +
- list(model_info.fields.keys()) +
- list(model_info.forward_relations.keys())
- )
-
- def _get_nested_class(self, nested_depth, relation_info):
- class NestedSerializer(ModelSerializer):
- class Meta:
- model = relation_info.related
- depth = nested_depth - 1
-
- return NestedSerializer
-
class HyperlinkedModelSerializer(ModelSerializer):
"""
@@ -1246,7 +1302,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
* A 'url' field is included instead of the 'id' field.
* Relationships to other instances are hyperlinks, instead of primary keys.
"""
- _related_class = HyperlinkedRelatedField
+ serializer_related_class = HyperlinkedRelatedField
def get_default_field_names(self, declared_fields, model_info):
"""
@@ -1260,10 +1316,17 @@ class HyperlinkedModelSerializer(ModelSerializer):
list(model_info.forward_relations.keys())
)
- def _get_nested_class(self, nested_depth, relation_info):
+ def build_nested_field(self, field_name, info, model, nested_depth):
+ """
+ Create nested fields for forward and reverse relationships.
+ """
+ relation_info = info.relations[field_name]
+
class NestedSerializer(HyperlinkedModelSerializer):
class Meta:
model = relation_info.related
depth = nested_depth - 1
- return NestedSerializer
+ field_cls = NestedSerializer
+ kwargs = get_nested_relation_kwargs(relation_info)
+ return field_cls, kwargs
From 62f78dfbf1b1dfa2d6406a4be5b83bc69267e851 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 15:50:29 +0000
Subject: [PATCH 028/121] Copy validators lists on instantiation.
---
rest_framework/serializers.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index a983d3fc7..8adbafe45 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -327,7 +327,9 @@ class Serializer(BaseSerializer):
Returns a list of validator callables.
"""
# Used by the lazily-evaluated `validators` property.
- return getattr(getattr(self, 'Meta', None), 'validators', [])
+ meta = getattr(self, 'Meta', None)
+ validators = getattr(meta, 'validators', None)
+ return validators[:] if validators else []
def get_initial(self):
if hasattr(self, 'initial_data'):
@@ -1213,7 +1215,7 @@ class ModelSerializer(Serializer):
# If the validators have been declared explicitly then use that.
validators = getattr(getattr(self, 'Meta', None), 'validators', None)
if validators is not None:
- return validators
+ return validators[:]
# Otherwise use the default set of validators.
return (
From 48d15f6ff8a13aafd5b4977c8d1b4b7fe70b4f6a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 16:58:35 +0000
Subject: [PATCH 029/121] Stub out the documentation
---
docs/api-guide/serializers.md | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index b9f0e7bc0..4d3dfa31b 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -567,6 +567,32 @@ The inner `Meta` class on serializers is not inherited from parent classes by de
Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
+## Advanced `ModelSerializer` usage
+
+The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer.
+
+#### `.serializer_field_mapping`
+
+A mapping of Django model classes to REST framework serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class.
+
+#### `.serializer_relational_field`
+
+This property should be the serializer field class, that is used for relational fields by default. For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `HyperlinkedModelSerializer` this defaults to `HyperlinkedRelatedField`.
+
+#### The build field methods
+
+#### `build_standard_field(**kwargs)`
+
+#### `build_relational_field(**kwargs)`
+
+#### `build_nested_field(**kwargs)`
+
+#### `build_property_field(**kwargs)`
+
+#### `build_url_field(**kwargs)`
+
+#### `build_unknown_field(**kwargs)`
+
---
# HyperlinkedModelSerializer
From 2a1485e00943b8280245d19e1e1f8514b1ef18ea Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 21:32:43 +0000
Subject: [PATCH 030/121] Final bits of docs for ModelSerializer fields API
---
docs/api-guide/serializers.md | 57 +++++++++---
docs_theme/css/default.css | 4 +
rest_framework/serializers.py | 140 ++++++++++++++++-------------
rest_framework/utils/model_meta.py | 10 +--
tests/test_model_serializer.py | 2 +-
5 files changed, 132 insertions(+), 81 deletions(-)
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 4d3dfa31b..dcbbd5f21 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -457,7 +457,7 @@ To do so, open the Django shell, using `python manage.py shell`, then import the
name = CharField(allow_blank=True, max_length=100, required=False)
owner = PrimaryKeyRelatedField(queryset=User.objects.all())
-## Specifying which fields should be included
+## Specifying which fields to include
If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`.
@@ -499,7 +499,7 @@ You can add extra fields to a `ModelSerializer` or override the default fields b
Extra fields can correspond to any property or callable on the model.
-## Specifying which fields should be read-only
+## Specifying read only fields
You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the shortcut Meta option, `read_only_fields`.
@@ -528,7 +528,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details
---
-## Specifying additional keyword arguments for fields.
+## Additional keyword arguments
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer.
@@ -567,31 +567,62 @@ The inner `Meta` class on serializers is not inherited from parent classes by de
Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
-## Advanced `ModelSerializer` usage
+## Customizing field mappings
The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer.
-#### `.serializer_field_mapping`
+Normally if a `ModelSerializer` does not generate the fields you need by default the you should either add them to the class explicitly, or simply use a regular `Serializer` class instead. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model.
+
+### `.serializer_field_mapping`
A mapping of Django model classes to REST framework serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class.
-#### `.serializer_relational_field`
+### `.serializer_relational_field`
This property should be the serializer field class, that is used for relational fields by default. For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`. For `HyperlinkedModelSerializer` this defaults to `HyperlinkedRelatedField`.
-#### The build field methods
+### The field_class and field_kwargs API
-#### `build_standard_field(**kwargs)`
+The following methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of `(field_class, field_kwargs)`.
-#### `build_relational_field(**kwargs)`
+### `.build_standard_field(self, field_name, model_field)`
-#### `build_nested_field(**kwargs)`
+Called to generate a serializer field that maps to a standard model field.
-#### `build_property_field(**kwargs)`
+The default implementation returns a serializer class based on the `serializer_field_mapping` attribute.
-#### `build_url_field(**kwargs)`
+### `.build_relational_field(self, field_name, relation_info)`
-#### `build_unknown_field(**kwargs)`
+Called to generate a serializer field that maps to a relational model field.
+
+The default implementation returns a serializer class based on the `serializer_relational_field` attribute.
+
+The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
+
+### `.build_nested_field(self, field_name, relation_info, nested_depth)`
+
+Called to generate a serializer field that maps to a relational model field, when the `depth` option has been set.
+
+The default implementation dynamically creates a nested serializer class based on either `ModelSerializer` or `HyperlinkedModelSerializer`.
+
+The `nested_depth` will be the value of the `depth` option, minus one.
+
+The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
+
+### `.build_property_field(self, field_name, model_class)`
+
+Called to generate a serializer field that maps to a property or zero-argument method on the model class.
+
+The default implementation returns a `ReadOnlyField` class.
+
+### `.build_url_field(self, field_name, model_class)`
+
+Called to generate a serializer field for the serializer's own `url` field. The default implementation returns a `HyperlinkedIdentityField` class.
+
+### `.build_unknown_field(self, field_name, model_class)`
+
+Called when the field name did not map to any model field or model property.
+The default implementation raises an error, although subclasses may customize this behavior.
---
diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css
index 8c9cd5363..48d00366b 100644
--- a/docs_theme/css/default.css
+++ b/docs_theme/css/default.css
@@ -239,6 +239,10 @@ body a:hover{
}
}
+h1 code, h2 code, h3 code, h4 code, h5 code {
+ color: #333;
+}
+
/* sticky footer and footer */
html, body {
height: 100%;
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8adbafe45..623ed5865 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -802,10 +802,25 @@ class ModelSerializer(Serializer):
Return the dict of field names -> field instances that should be
used for `self.fields` when instantiating the serializer.
"""
+ assert hasattr(self, 'Meta'), (
+ 'Class {serializer_class} missing "Meta" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+ assert hasattr(self.Meta, 'model'), (
+ 'Class {serializer_class} missing "Meta.model" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
declared_fields = copy.deepcopy(self._declared_fields)
model = getattr(self.Meta, 'model')
depth = getattr(self.Meta, 'depth', 0)
+ if depth is not None:
+ assert depth >= 0, "'depth' may not be negative."
+ assert depth <= 10, "'depth' may not be greater than 10."
+
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
field_names = self.get_field_names(declared_fields, info)
@@ -817,27 +832,32 @@ class ModelSerializer(Serializer):
field_names, declared_fields, extra_kwargs
)
- # Now determine the fields that should be included on the serializer.
- ret = OrderedDict()
+ # Determine the fields that should be included on the serializer.
+ fields = OrderedDict()
+
for field_name in field_names:
+ # If the field is explicitly declared on the class then use that.
if field_name in declared_fields:
- # Field is explicitly declared on the class, use that.
- ret[field_name] = declared_fields[field_name]
+ fields[field_name] = declared_fields[field_name]
continue
# Determine the serializer field class and keyword arguments.
- field_cls, kwargs = self.build_field(field_name, info, model, depth)
+ field_class, field_kwargs = self.build_field(
+ field_name, info, model, depth
+ )
- # Populate any kwargs defined in `Meta.extra_kwargs`
- kwargs = self.build_field_kwargs(kwargs, extra_kwargs, field_name)
+ # Include any kwargs defined in `Meta.extra_kwargs`
+ field_kwargs = self.build_field_kwargs(
+ field_kwargs, extra_kwargs, field_name
+ )
# Create the serializer field.
- ret[field_name] = field_cls(**kwargs)
+ fields[field_name] = field_class(**field_kwargs)
# Add in any hidden fields.
- ret.update(hidden_fields)
+ fields.update(hidden_fields)
- return ret
+ return fields
# Methods for determining the set of field names to include...
@@ -916,108 +936,105 @@ class ModelSerializer(Serializer):
# Methods for constructing serializer fields...
- def build_field(self, field_name, info, model, nested_depth):
+ def build_field(self, field_name, info, model_class, nested_depth):
"""
Return a two tuple of (cls, kwargs) to build a serializer field with.
"""
if field_name in info.fields_and_pk:
- return self.build_standard_field(field_name, info, model)
+ model_field = info.fields_and_pk[field_name]
+ return self.build_standard_field(field_name, model_field)
elif field_name in info.relations:
+ relation_info = info.relations[field_name]
if not nested_depth:
- return self.build_relational_field(field_name, info, model)
+ return self.build_relational_field(field_name, relation_info)
else:
- return self.build_nested_field(field_name, info, model, nested_depth)
+ return self.build_nested_field(field_name, relation_info, nested_depth)
- elif hasattr(model, field_name):
- return self.build_property_field(field_name, info, model)
+ elif hasattr(model_class, field_name):
+ return self.build_property_field(field_name, model_class)
elif field_name == api_settings.URL_FIELD_NAME:
- return self.build_url_field(field_name, info, model)
+ return self.build_url_field(field_name, model_class)
- return self.build_unknown_field(field_name, info, model)
+ return self.build_unknown_field(field_name, model_class)
- def build_standard_field(self, field_name, info, model):
+ def build_standard_field(self, field_name, model_field):
"""
Create regular model fields.
"""
field_mapping = ClassLookupDict(self.serializer_field_mapping)
- model_field = info.fields_and_pk[field_name]
- field_cls = field_mapping[model_field]
- kwargs = get_field_kwargs(field_name, model_field)
+ field_class = field_mapping[model_field]
+ field_kwargs = get_field_kwargs(field_name, model_field)
- if 'choices' in kwargs:
+ if 'choices' in field_kwargs:
# Fields with choices get coerced into `ChoiceField`
# instead of using their regular typed field.
- field_cls = ChoiceField
- if not issubclass(field_cls, ModelField):
+ field_class = ChoiceField
+ if not issubclass(field_class, ModelField):
# `model_field` is only valid for the fallback case of
# `ModelField`, which is used when no other typed field
# matched to the model field.
- kwargs.pop('model_field', None)
- if not issubclass(field_cls, CharField) and not issubclass(field_cls, ChoiceField):
+ field_kwargs.pop('model_field', None)
+ if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField):
# `allow_blank` is only valid for textual fields.
- kwargs.pop('allow_blank', None)
+ field_kwargs.pop('allow_blank', None)
- return field_cls, kwargs
+ return field_class, field_kwargs
- def build_relational_field(self, field_name, info, model):
+ def build_relational_field(self, field_name, relation_info):
"""
Create fields for forward and reverse relationships.
"""
- relation_info = info.relations[field_name]
-
- field_cls = self.serializer_related_class
- kwargs = get_relation_kwargs(field_name, relation_info)
+ field_class = self.serializer_related_class
+ field_kwargs = get_relation_kwargs(field_name, relation_info)
# `view_name` is only valid for hyperlinked relationships.
- if not issubclass(field_cls, HyperlinkedRelatedField):
- kwargs.pop('view_name', None)
+ if not issubclass(field_class, HyperlinkedRelatedField):
+ field_kwargs.pop('view_name', None)
- return field_cls, kwargs
+ return field_class, field_kwargs
- def build_nested_field(self, field_name, info, model, nested_depth):
+ def build_nested_field(self, field_name, relation_info, nested_depth):
"""
Create nested fields for forward and reverse relationships.
"""
- relation_info = info.relations[field_name]
-
class NestedSerializer(ModelSerializer):
class Meta:
- model = relation_info.related
- depth = nested_depth - 1
+ model = relation_info.related_model
+ depth = nested_depth
- field_cls = NestedSerializer
- kwargs = get_nested_relation_kwargs(relation_info)
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- return field_cls, kwargs
+ return field_class, field_kwargs
- def build_property_field(self, field_name, info, model):
+ def build_property_field(self, field_name, model_class):
"""
Create a read only field for model methods and properties.
"""
- field_cls = ReadOnlyField
- kwargs = {}
+ field_class = ReadOnlyField
+ field_kwargs = {}
- return field_cls, kwargs
+ return field_class, field_kwargs
- def build_url_field(self, field_name, info, model):
+ def build_url_field(self, field_name, model_class):
"""
Create a field representing the object's own URL.
"""
- field_cls = HyperlinkedIdentityField
- kwargs = get_url_kwargs(model)
+ field_class = HyperlinkedIdentityField
+ field_kwargs = get_url_kwargs(model_class)
- return field_cls, kwargs
+ return field_class, field_kwargs
- def build_unknown_field(self, field_name, info, model):
+ def build_unknown_field(self, field_name, model_class):
"""
Raise an error on any unknown fields.
"""
raise ImproperlyConfigured(
'Field name `%s` is not valid for model `%s`.' %
- (field_name, model.__class__.__name__)
+ (field_name, model_class.__name__)
)
def build_field_kwargs(self, kwargs, extra_kwargs, field_name):
@@ -1318,17 +1335,16 @@ class HyperlinkedModelSerializer(ModelSerializer):
list(model_info.forward_relations.keys())
)
- def build_nested_field(self, field_name, info, model, nested_depth):
+ def build_nested_field(self, field_name, relation_info, nested_depth):
"""
Create nested fields for forward and reverse relationships.
"""
- relation_info = info.relations[field_name]
-
class NestedSerializer(HyperlinkedModelSerializer):
class Meta:
- model = relation_info.related
+ model = relation_info.related_model
depth = nested_depth - 1
- field_cls = NestedSerializer
- kwargs = get_nested_relation_kwargs(relation_info)
- return field_cls, kwargs
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
+
+ return field_class, field_kwargs
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index c98725c66..dfc387ca5 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -24,7 +24,7 @@ FieldInfo = namedtuple('FieldResult', [
RelationInfo = namedtuple('RelationInfo', [
'model_field',
- 'related',
+ 'related_model',
'to_many',
'has_through_model'
])
@@ -77,7 +77,7 @@ def get_field_info(model):
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
model_field=field,
- related=_resolve_model(field.rel.to),
+ related_model=_resolve_model(field.rel.to),
to_many=False,
has_through_model=False
)
@@ -86,7 +86,7 @@ def get_field_info(model):
for field in [field for field in opts.many_to_many if field.serialize]:
forward_relations[field.name] = RelationInfo(
model_field=field,
- related=_resolve_model(field.rel.to),
+ related_model=_resolve_model(field.rel.to),
to_many=True,
has_through_model=(
not field.rel.through._meta.auto_created
@@ -99,7 +99,7 @@ def get_field_info(model):
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
model_field=None,
- related=relation.model,
+ related_model=relation.model,
to_many=relation.field.rel.multiple,
has_through_model=False
)
@@ -109,7 +109,7 @@ def get_field_info(model):
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
model_field=None,
- related=relation.model,
+ related_model=relation.model,
to_many=True,
has_through_model=(
(getattr(relation.field.rel, 'through', None) is not None)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 5c56c8dbb..603faf477 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -206,7 +206,7 @@ class TestRegularFieldMappings(TestCase):
with self.assertRaises(ImproperlyConfigured) as excinfo:
TestSerializer().fields
- expected = 'Field name `invalid` is not valid for model `ModelBase`.'
+ expected = 'Field name `invalid` is not valid for model `RegularFieldsModel`.'
assert str(excinfo.exception) == expected
def test_missing_field(self):
From 309b5d264166e07510d6cbc54d681268d07957aa Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Fri, 2 Jan 2015 11:07:35 +0000
Subject: [PATCH 031/121] instructions on how to translate REST framework error
messages
---
docs/topics/internationalisation.md | 34 +++++++++++++++++++++++++++++
1 file changed, 34 insertions(+)
create mode 100644 docs/topics/internationalisation.md
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
new file mode 100644
index 000000000..01f968915
--- /dev/null
+++ b/docs/topics/internationalisation.md
@@ -0,0 +1,34 @@
+# Internationalisation
+REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms](https://docs.djangoproject.com/en/1.7/topics/i18n/translation) and by translating the messages into your language.
+
+## How to translate REST Framework errors
+
+
+This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs](https://docs.djangoproject.com/en/1.7/topics/i18n/translation).
+
+
+#### To translate REST framework error messages:
+
+1. Pick an app where you want the translations to be, for example `myapp`
+
+2. Add a symlink from that app to the installed `rest_framework`
+ ```
+ ln -s /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/ rest_framework
+ ```
+
+ To find out where `rest_framework` is installed, run
+
+ ```
+ python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
+ ```
+
+3. Run Django's `makemessages` command in the normal way, but add the `--symlink` option. For example, if you want to translate into Brazilian Portuguese you would run
+ ```
+ manage.py makemessages --symlink -l pt_BR
+ ```
+
+4. Translate the `django.po` file which is created as normal. This will be in the folder `myapp/locale/pt_BR/LC_MESSAGES`.
+
+5. Run `manage.py compilemessages` as normal
+
+6. Restart your server
From 7ad7dd6a4292157ed5bcbaacb60b6ccc93fcf201 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 24 Dec 2014 18:26:17 +0000
Subject: [PATCH 032/121] match DRF style guide
---
docs/topics/internationalisation.md | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
index 01f968915..552fdd273 100644
--- a/docs/topics/internationalisation.md
+++ b/docs/topics/internationalisation.md
@@ -1,10 +1,10 @@
# Internationalisation
-REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms](https://docs.djangoproject.com/en/1.7/topics/i18n/translation) and by translating the messages into your language.
+REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation] and by translating the messages into your language.
## How to translate REST Framework errors
-This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs](https://docs.djangoproject.com/en/1.7/topics/i18n/translation).
+This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
#### To translate REST framework error messages:
@@ -16,19 +16,28 @@ This guide assumes you are already familiar with how to translate a Django app.
ln -s /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/ rest_framework
```
- To find out where `rest_framework` is installed, run
+ ---
+
+ **Note:** To find out where `rest_framework` is installed, run
```
python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
```
-3. Run Django's `makemessages` command in the normal way, but add the `--symlink` option. For example, if you want to translate into Brazilian Portuguese you would run
+ ---
+
+
+
+3. Run Django's `makemessages` command in the normal way, but add the `--symlink` option. For example, if you want to translate into Brazilian Portuguese you would run
```
manage.py makemessages --symlink -l pt_BR
```
-4. Translate the `django.po` file which is created as normal. This will be in the folder `myapp/locale/pt_BR/LC_MESSAGES`.
+4. Translate the `django.po` file which is created as normal. This will be in the folder `myapp/locale/pt_BR/LC_MESSAGES`.
5. Run `manage.py compilemessages` as normal
6. Restart your server
+
+
+[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
From 2781903a5a0be7f5314de54ff2dbc8ef393eab0a Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Fri, 26 Dec 2014 12:52:17 +0000
Subject: [PATCH 033/121] Add info about how django chooses which language to
use
---
docs/topics/internationalisation.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
index 552fdd273..a0aab7533 100644
--- a/docs/topics/internationalisation.md
+++ b/docs/topics/internationalisation.md
@@ -40,4 +40,22 @@ This guide assumes you are already familiar with how to translate a Django app.
6. Restart your server
+
+## How Django chooses which language to use
+REST framework will use the same preferences to select which language to display as Django does. You can find more info in the [django docs on discovering language preferences][django-language-preference]. For reference, these are
+
+1. First, it looks for the language prefix in the requested URL
+2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
+3. Failing that, it looks for a cookie
+4. Failing that, it looks at the `Accept-Language` HTTP header.
+5. Failing that, it uses the global `LANGUAGE_CODE` setting.
+
+---
+
+**Note:** You'll need to include the `django.middleware.locale.LocaleMiddleware` to enable any of the per-request language preferences.
+
+---
+
+
[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
+[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
From 0b8a83bd624673cb0a05e01c691729ccee3a8782 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Sun, 28 Dec 2014 18:20:41 +0000
Subject: [PATCH 034/121] update internationalisation instructions to prevent
symlinking; add base .po file
---
docs/topics/internationalisation.md | 52 +++-
.../locale/en_US/LC_MESSAGES/django.po | 277 ++++++++++++++++++
2 files changed, 318 insertions(+), 11 deletions(-)
create mode 100644 rest_framework/locale/en_US/LC_MESSAGES/django.po
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
index a0aab7533..6470ee033 100644
--- a/docs/topics/internationalisation.md
+++ b/docs/topics/internationalisation.md
@@ -9,12 +9,40 @@ This guide assumes you are already familiar with how to translate a Django app.
#### To translate REST framework error messages:
-1. Pick an app where you want the translations to be, for example `myapp`
+1. Make a new folder where you want to store the translated errors. Add this
+path to your [`LOCALE_PATHS`][django-locale-paths] setting.
+
+ ---
+
+ **Note:** For the rest of
+this document we will assume the path you created was
+`/home/www/project/conf/locale/`, and that you have updated your `settings.py` to include the setting:
-2. Add a symlink from that app to the installed `rest_framework`
```
- ln -s /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/ rest_framework
+ LOCALE_PATHS = (
+ '/home/www/project/conf/locale/',
+ )
```
+
+ ---
+
+2. Now create a subfolder for the language you want to translate. The folder should be named using [locale
+name][django-locale-name] notation. E.g. `de`, `pt_BR`, `es_AR`, etc.
+
+ ```
+ mkdir /home/www/project/conf/locale/pt_BR/LC_MESSAGES
+ ```
+
+3. Now copy the base translations file from the REST framework source code
+into your translations folder
+
+ ```
+ cp /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/locale/en_US/LC_MESSAGES/django.po
+ /home/www/project/conf/locale/pt_BR/LC_MESSAGES
+ ```
+
+ This should create the file
+ `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po`
---
@@ -27,17 +55,17 @@ This guide assumes you are already familiar with how to translate a Django app.
---
+4. Edit `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po` and
+translate all the error messages.
-3. Run Django's `makemessages` command in the normal way, but add the `--symlink` option. For example, if you want to translate into Brazilian Portuguese you would run
- ```
- manage.py makemessages --symlink -l pt_BR
- ```
-
-4. Translate the `django.po` file which is created as normal. This will be in the folder `myapp/locale/pt_BR/LC_MESSAGES`.
+5. Run `manage.py compilemessages -l pt_BR` to make the translations
+available for Django to use. You should see a message
-5. Run `manage.py compilemessages` as normal
+ ```
+ processing file django.po in /home/www/project/conf/locale/pt_BR/LC_MESSAGES
+ ```
-6. Restart your server
+6. Restart your server.
@@ -59,3 +87,5 @@ REST framework will use the same preferences to select which language to display
[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
+[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
+[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
\ No newline at end of file
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
new file mode 100644
index 000000000..510ce0aad
--- /dev/null
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -0,0 +1,277 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-12-28 17:49+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: rest_framework/authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: rest_framework/authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: rest_framework/authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\""
+msgstr ""
+
+#: rest_framework/exceptions.py:39
+msgid "A server error occured"
+msgstr ""
+
+#: rest_framework/exceptions.py:74
+msgid "Malformed request."
+msgstr ""
+
+#: rest_framework/exceptions.py:79
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: rest_framework/exceptions.py:84
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: rest_framework/exceptions.py:89
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: rest_framework/exceptions.py:94
+#, python-format
+msgid "Method '%s' not allowed."
+msgstr ""
+
+#: rest_framework/exceptions.py:105
+msgid "Could not satisfy the request Accept header"
+msgstr ""
+
+#: rest_framework/exceptions.py:117
+#, python-format
+msgid "Unsupported media type '%s' in request."
+msgstr ""
+
+#: rest_framework/exceptions.py:128
+msgid "Request was throttled."
+msgstr ""
+
+#: rest_framework/exceptions.py:130
+#, python-format
+msgid "Expected available in %(wait)d second."
+msgid_plural "Expected available in %(wait)d seconds."
+msgstr[0] ""
+msgstr[1] ""
+
+#: rest_framework/fields.py:152 rest_framework/relations.py:131
+#: rest_framework/relations.py:155 rest_framework/validators.py:77
+#: rest_framework/validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: rest_framework/fields.py:153
+msgid "This field may not be null."
+msgstr ""
+
+#: rest_framework/fields.py:484 rest_framework/fields.py:512
+msgid "`{input}` is not a valid boolean."
+msgstr ""
+
+#: rest_framework/fields.py:547
+msgid "This field may not be blank."
+msgstr ""
+
+#: rest_framework/fields.py:548 rest_framework/fields.py:1250
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: rest_framework/fields.py:549
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: rest_framework/fields.py:584
+msgid "Enter a valid email address."
+msgstr ""
+
+#: rest_framework/fields.py:601
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: rest_framework/fields.py:612
+msgid ""
+"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
+msgstr ""
+
+#: rest_framework/fields.py:624
+msgid "Enter a valid URL."
+msgstr ""
+
+#: rest_framework/fields.py:637
+msgid "A valid integer is required."
+msgstr ""
+
+#: rest_framework/fields.py:638 rest_framework/fields.py:672
+#: rest_framework/fields.py:705
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: rest_framework/fields.py:639 rest_framework/fields.py:673
+#: rest_framework/fields.py:706
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: rest_framework/fields.py:640 rest_framework/fields.py:674
+#: rest_framework/fields.py:710
+msgid "String value too large"
+msgstr ""
+
+#: rest_framework/fields.py:671 rest_framework/fields.py:704
+msgid "A valid number is required."
+msgstr ""
+
+#: rest_framework/fields.py:707
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: rest_framework/fields.py:708
+msgid "Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: rest_framework/fields.py:709
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: rest_framework/fields.py:793
+msgid "Datetime has wrong format. Use one of these formats instead: {format}"
+msgstr ""
+
+#: rest_framework/fields.py:794
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: rest_framework/fields.py:858
+msgid "Date has wrong format. Use one of these formats instead: {format}"
+msgstr ""
+
+#: rest_framework/fields.py:859
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: rest_framework/fields.py:916
+msgid "Time has wrong format. Use one of these formats instead: {format}"
+msgstr ""
+
+#: rest_framework/fields.py:972 rest_framework/fields.py:1016
+msgid "`{input}` is not a valid choice."
+msgstr ""
+
+#: rest_framework/fields.py:1017 rest_framework/serializers.py:474
+msgid "Expected a list of items but got type `{input_type}`."
+msgstr ""
+
+#: rest_framework/fields.py:1047
+msgid "No file was submitted."
+msgstr ""
+
+#: rest_framework/fields.py:1048
+msgid "The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: rest_framework/fields.py:1049
+msgid "No filename could be determined."
+msgstr ""
+
+#: rest_framework/fields.py:1050
+msgid "The submitted file is empty."
+msgstr ""
+
+#: rest_framework/fields.py:1051
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: rest_framework/fields.py:1093
+msgid "Upload a valid image. The file you uploaded was either not an "
+msgstr ""
+
+#: rest_framework/fields.py:1119
+msgid "Expected a list of items but got type `{input_type}`"
+msgstr ""
+
+#: rest_framework/generics.py:122
+msgid "Page is not 'last', nor can it be converted to an int."
+msgstr ""
+
+#: rest_framework/generics.py:126
+#, python-format
+msgid "Invalid page (%(page_number)s): %(message)s"
+msgstr ""
+
+#: rest_framework/relations.py:132
+msgid "Invalid pk '{pk_value}' - object does not exist."
+msgstr ""
+
+#: rest_framework/relations.py:133
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: rest_framework/relations.py:156
+msgid "Invalid hyperlink - No URL match"
+msgstr ""
+
+#: rest_framework/relations.py:157
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: rest_framework/relations.py:158
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: rest_framework/relations.py:159
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: rest_framework/relations.py:294
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: rest_framework/relations.py:295
+msgid "Invalid value."
+msgstr ""
+
+#: rest_framework/serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: rest_framework/validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: rest_framework/validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: rest_framework/validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: rest_framework/validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: rest_framework/validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
From faf76a4b75f12f3fa9de4e3ec455daa239af4d89 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 31 Dec 2014 12:49:20 +0000
Subject: [PATCH 035/121] fix spelling & grammar errors
---
rest_framework/exceptions.py | 2 +-
rest_framework/generics.py | 2 +-
rest_framework/locale/en_US/LC_MESSAGES/django.po | 8 ++++----
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index bcfd8961b..2586fc332 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -36,7 +36,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = _('A server error occured')
+ default_detail = _('A server error occurred')
def __init__(self, detail=None):
if detail is not None:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index e6db155e7..bdbc19a75 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -119,7 +119,7 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise Http404(_("Page is not 'last', nor can it be converted to an int."))
+ raise Http404(_("Page is not 'last', and cannot be converted to an int."))
try:
page = paginator.page(page_number)
except InvalidPage as exc:
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 510ce0aad..3bed91430 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -2,13 +2,13 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
-#
+#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-12-28 17:49+0000\n"
+"POT-Creation-Date: 2014-12-31 12:48+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -30,7 +30,7 @@ msgid "Must include \"username\" and \"password\""
msgstr ""
#: rest_framework/exceptions.py:39
-msgid "A server error occured"
+msgid "A server error occurred"
msgstr ""
#: rest_framework/exceptions.py:74
@@ -212,7 +212,7 @@ msgid "Expected a list of items but got type `{input_type}`"
msgstr ""
#: rest_framework/generics.py:122
-msgid "Page is not 'last', nor can it be converted to an int."
+msgid "Page is not 'last', and cannot be converted to an int."
msgstr ""
#: rest_framework/generics.py:126
From a90ba2bc11de5fb391b95d4fce84f87ae7f88eff Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 31 Dec 2014 13:03:16 +0000
Subject: [PATCH 036/121] update error messages for language and consistency
---
rest_framework/exceptions.py | 4 +--
rest_framework/fields.py | 17 +++++-----
rest_framework/generics.py | 2 +-
.../locale/en_US/LC_MESSAGES/django.po | 33 ++++++++++---------
4 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 2586fc332..d78b7e975 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -36,7 +36,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = _('A server error occurred')
+ default_detail = _('A server error occurred.')
def __init__(self, detail=None):
if detail is not None:
@@ -107,7 +107,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = _('Could not satisfy the request Accept header')
+ default_detail = _('Could not satisfy the request Accept header.')
def __init__(self, detail=None, available_renderers=None):
if detail is not None:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index c40dc3fb3..0ff2b0733 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -640,7 +640,7 @@ class IntegerField(Field):
'invalid': _('A valid integer is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
- 'max_string_length': _('String value too large')
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -674,7 +674,7 @@ class FloatField(Field):
'invalid': _("A valid number is required."),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
- 'max_string_length': _('String value too large')
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -710,7 +710,7 @@ class DecimalField(Field):
'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'),
- 'max_string_length': _('String value too large')
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -793,7 +793,7 @@ class DecimalField(Field):
class DateTimeField(Field):
default_error_messages = {
- 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
'date': _('Expected a datetime but got a date.'),
}
format = api_settings.DATETIME_FORMAT
@@ -858,7 +858,7 @@ class DateTimeField(Field):
class DateField(Field):
default_error_messages = {
- 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
'datetime': _('Expected a date but got a datetime.'),
}
format = api_settings.DATE_FORMAT
@@ -916,7 +916,7 @@ class DateField(Field):
class TimeField(Field):
default_error_messages = {
- 'invalid': _('Time has wrong format. Use one of these formats instead: {format}'),
+ 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
}
format = api_settings.TIME_FORMAT
input_formats = api_settings.TIME_INPUT_FORMATS
@@ -1093,8 +1093,7 @@ class FileField(Field):
class ImageField(FileField):
default_error_messages = {
'invalid_image': _(
- 'Upload a valid image. The file you uploaded was either not an '
- 'image or a corrupted image.'
+ 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
),
}
@@ -1119,7 +1118,7 @@ class ListField(Field):
child = None
initial = []
default_error_messages = {
- 'not_a_list': _('Expected a list of items but got type `{input_type}`')
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
}
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index bdbc19a75..680992d75 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -119,7 +119,7 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise Http404(_("Page is not 'last', and cannot be converted to an int."))
+ raise Http404(_("Choose a valid page number. Page numbers must be a whole number, or must be the string 'last'."))
try:
page = paginator.page(page_number)
except InvalidPage as exc:
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 3bed91430..18f5fe18d 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -2,13 +2,13 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR , YEAR.
-#
+#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-12-31 12:48+0000\n"
+"POT-Creation-Date: 2014-12-31 13:02+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -30,7 +30,7 @@ msgid "Must include \"username\" and \"password\""
msgstr ""
#: rest_framework/exceptions.py:39
-msgid "A server error occurred"
+msgid "A server error occurred."
msgstr ""
#: rest_framework/exceptions.py:74
@@ -55,7 +55,7 @@ msgid "Method '%s' not allowed."
msgstr ""
#: rest_framework/exceptions.py:105
-msgid "Could not satisfy the request Accept header"
+msgid "Could not satisfy the request Accept header."
msgstr ""
#: rest_framework/exceptions.py:117
@@ -92,7 +92,7 @@ msgstr ""
msgid "This field may not be blank."
msgstr ""
-#: rest_framework/fields.py:548 rest_framework/fields.py:1250
+#: rest_framework/fields.py:548 rest_framework/fields.py:1249
msgid "Ensure this field has no more than {max_length} characters."
msgstr ""
@@ -133,7 +133,7 @@ msgstr ""
#: rest_framework/fields.py:640 rest_framework/fields.py:674
#: rest_framework/fields.py:710
-msgid "String value too large"
+msgid "String value too large."
msgstr ""
#: rest_framework/fields.py:671 rest_framework/fields.py:704
@@ -155,7 +155,7 @@ msgid ""
msgstr ""
#: rest_framework/fields.py:793
-msgid "Datetime has wrong format. Use one of these formats instead: {format}"
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
msgstr ""
#: rest_framework/fields.py:794
@@ -163,7 +163,7 @@ msgid "Expected a datetime but got a date."
msgstr ""
#: rest_framework/fields.py:858
-msgid "Date has wrong format. Use one of these formats instead: {format}"
+msgid "Date has wrong format. Use one of these formats instead: {format}."
msgstr ""
#: rest_framework/fields.py:859
@@ -171,14 +171,15 @@ msgid "Expected a date but got a datetime."
msgstr ""
#: rest_framework/fields.py:916
-msgid "Time has wrong format. Use one of these formats instead: {format}"
+msgid "Time has wrong format. Use one of these formats instead: {format}."
msgstr ""
#: rest_framework/fields.py:972 rest_framework/fields.py:1016
msgid "`{input}` is not a valid choice."
msgstr ""
-#: rest_framework/fields.py:1017 rest_framework/serializers.py:474
+#: rest_framework/fields.py:1017 rest_framework/fields.py:1118
+#: rest_framework/serializers.py:474
msgid "Expected a list of items but got type `{input_type}`."
msgstr ""
@@ -204,15 +205,15 @@ msgid ""
msgstr ""
#: rest_framework/fields.py:1093
-msgid "Upload a valid image. The file you uploaded was either not an "
-msgstr ""
-
-#: rest_framework/fields.py:1119
-msgid "Expected a list of items but got type `{input_type}`"
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
msgstr ""
#: rest_framework/generics.py:122
-msgid "Page is not 'last', and cannot be converted to an int."
+msgid ""
+"Choose a valid page number. Page numbers must be a whole number, or must be "
+"the string 'last'."
msgstr ""
#: rest_framework/generics.py:126
From 32506e20756c84677abb5ae49706446a0d250371 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 31 Dec 2014 13:14:09 +0000
Subject: [PATCH 037/121] update expected error messages in tests
---
tests/test_fields.py | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 04c721d36..61d39aff6 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -640,8 +640,8 @@ class TestDateField(FieldValues):
datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
}
invalid_inputs = {
- 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
- '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
}
outputs = {
@@ -658,7 +658,7 @@ class TestCustomInputFormatDateField(FieldValues):
'1 Jan 2001': datetime.date(2001, 1, 1),
}
invalid_inputs = {
- '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY.']
}
outputs = {}
field = serializers.DateField(input_formats=['%d %b %Y'])
@@ -702,8 +702,8 @@ class TestDateTimeField(FieldValues):
'2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
}
invalid_inputs = {
- 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
- '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
}
outputs = {
@@ -721,7 +721,7 @@ class TestCustomInputFormatDateTimeField(FieldValues):
'1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
}
invalid_inputs = {
- '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
}
outputs = {}
field = serializers.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
@@ -773,8 +773,8 @@ class TestTimeField(FieldValues):
datetime.time(13, 00): datetime.time(13, 00),
}
invalid_inputs = {
- 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
- '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
}
outputs = {
datetime.time(13, 00): '13:00:00'
@@ -790,7 +790,7 @@ class TestCustomInputFormatTimeField(FieldValues):
'1:00pm': datetime.time(13, 00),
}
invalid_inputs = {
- '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM].'],
}
outputs = {}
field = serializers.TimeField(input_formats=['%I:%M%p'])
@@ -1028,7 +1028,7 @@ class TestListField(FieldValues):
(['1', '2', '3'], [1, 2, 3])
]
invalid_inputs = [
- ('not a list', ['Expected a list of items but got type `str`']),
+ ('not a list', ['Expected a list of items but got type `str`.']),
([1, 2, 'error'], ['A valid integer is required.'])
]
outputs = [
From 9f169acb62a6223a5add0fee7f6d53108e42f207 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 31 Dec 2014 14:56:23 +0000
Subject: [PATCH 038/121] capitalise Django
---
docs/topics/internationalisation.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
index 6470ee033..fac3bdb7a 100644
--- a/docs/topics/internationalisation.md
+++ b/docs/topics/internationalisation.md
@@ -70,7 +70,8 @@ available for Django to use. You should see a message
## How Django chooses which language to use
-REST framework will use the same preferences to select which language to display as Django does. You can find more info in the [django docs on discovering language preferences][django-language-preference]. For reference, these are
+REST framework will use the same preferences to select which language to
+display as Django does. You can find more info in the [Django docs on discovering language preferences][django-language-preference]. For reference, these are
1. First, it looks for the language prefix in the requested URL
2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
From 6fb37207d18949031fb7203d6fd67ee503df0a34 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Fri, 2 Jan 2015 11:11:13 +0000
Subject: [PATCH 039/121] add missing period; update generated translations
---
rest_framework/exceptions.py | 2 +-
.../locale/en_US/LC_MESSAGES/django.po | 96 +++++++++++--------
2 files changed, 59 insertions(+), 39 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index d78b7e975..c8cedfceb 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -91,7 +91,7 @@ class PermissionDenied(APIException):
class NotFound(APIException):
status_code = status.HTTP_404_NOT_FOUND
- default_detail = _('Not found')
+ default_detail = _('Not found.')
class MethodNotAllowed(APIException):
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 18f5fe18d..569020739 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-12-31 13:02+0000\n"
+"POT-Creation-Date: 2015-01-02 11:10+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -50,24 +50,28 @@ msgid "You do not have permission to perform this action."
msgstr ""
#: rest_framework/exceptions.py:94
+msgid "Not found."
+msgstr ""
+
+#: rest_framework/exceptions.py:99
#, python-format
msgid "Method '%s' not allowed."
msgstr ""
-#: rest_framework/exceptions.py:105
+#: rest_framework/exceptions.py:110
msgid "Could not satisfy the request Accept header."
msgstr ""
-#: rest_framework/exceptions.py:117
+#: rest_framework/exceptions.py:122
#, python-format
msgid "Unsupported media type '%s' in request."
msgstr ""
-#: rest_framework/exceptions.py:128
+#: rest_framework/exceptions.py:133
msgid "Request was throttled."
msgstr ""
-#: rest_framework/exceptions.py:130
+#: rest_framework/exceptions.py:135
#, python-format
msgid "Expected available in %(wait)d second."
msgid_plural "Expected available in %(wait)d seconds."
@@ -84,127 +88,127 @@ msgstr ""
msgid "This field may not be null."
msgstr ""
-#: rest_framework/fields.py:484 rest_framework/fields.py:512
+#: rest_framework/fields.py:480 rest_framework/fields.py:508
msgid "`{input}` is not a valid boolean."
msgstr ""
-#: rest_framework/fields.py:547
+#: rest_framework/fields.py:543
msgid "This field may not be blank."
msgstr ""
-#: rest_framework/fields.py:548 rest_framework/fields.py:1249
+#: rest_framework/fields.py:544 rest_framework/fields.py:1252
msgid "Ensure this field has no more than {max_length} characters."
msgstr ""
-#: rest_framework/fields.py:549
+#: rest_framework/fields.py:545
msgid "Ensure this field has at least {min_length} characters."
msgstr ""
-#: rest_framework/fields.py:584
+#: rest_framework/fields.py:587
msgid "Enter a valid email address."
msgstr ""
-#: rest_framework/fields.py:601
+#: rest_framework/fields.py:604
msgid "This value does not match the required pattern."
msgstr ""
-#: rest_framework/fields.py:612
+#: rest_framework/fields.py:615
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
-#: rest_framework/fields.py:624
+#: rest_framework/fields.py:627
msgid "Enter a valid URL."
msgstr ""
-#: rest_framework/fields.py:637
+#: rest_framework/fields.py:640
msgid "A valid integer is required."
msgstr ""
-#: rest_framework/fields.py:638 rest_framework/fields.py:672
-#: rest_framework/fields.py:705
+#: rest_framework/fields.py:641 rest_framework/fields.py:675
+#: rest_framework/fields.py:708
msgid "Ensure this value is less than or equal to {max_value}."
msgstr ""
-#: rest_framework/fields.py:639 rest_framework/fields.py:673
-#: rest_framework/fields.py:706
+#: rest_framework/fields.py:642 rest_framework/fields.py:676
+#: rest_framework/fields.py:709
msgid "Ensure this value is greater than or equal to {min_value}."
msgstr ""
-#: rest_framework/fields.py:640 rest_framework/fields.py:674
-#: rest_framework/fields.py:710
+#: rest_framework/fields.py:643 rest_framework/fields.py:677
+#: rest_framework/fields.py:713
msgid "String value too large."
msgstr ""
-#: rest_framework/fields.py:671 rest_framework/fields.py:704
+#: rest_framework/fields.py:674 rest_framework/fields.py:707
msgid "A valid number is required."
msgstr ""
-#: rest_framework/fields.py:707
+#: rest_framework/fields.py:710
msgid "Ensure that there are no more than {max_digits} digits in total."
msgstr ""
-#: rest_framework/fields.py:708
+#: rest_framework/fields.py:711
msgid "Ensure that there are no more than {max_decimal_places} decimal places."
msgstr ""
-#: rest_framework/fields.py:709
+#: rest_framework/fields.py:712
msgid ""
"Ensure that there are no more than {max_whole_digits} digits before the "
"decimal point."
msgstr ""
-#: rest_framework/fields.py:793
+#: rest_framework/fields.py:796
msgid "Datetime has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:794
+#: rest_framework/fields.py:797
msgid "Expected a datetime but got a date."
msgstr ""
-#: rest_framework/fields.py:858
+#: rest_framework/fields.py:861
msgid "Date has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:859
+#: rest_framework/fields.py:862
msgid "Expected a date but got a datetime."
msgstr ""
-#: rest_framework/fields.py:916
+#: rest_framework/fields.py:919
msgid "Time has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:972 rest_framework/fields.py:1016
+#: rest_framework/fields.py:975 rest_framework/fields.py:1019
msgid "`{input}` is not a valid choice."
msgstr ""
-#: rest_framework/fields.py:1017 rest_framework/fields.py:1118
-#: rest_framework/serializers.py:474
+#: rest_framework/fields.py:1020 rest_framework/fields.py:1121
+#: rest_framework/serializers.py:476
msgid "Expected a list of items but got type `{input_type}`."
msgstr ""
-#: rest_framework/fields.py:1047
+#: rest_framework/fields.py:1050
msgid "No file was submitted."
msgstr ""
-#: rest_framework/fields.py:1048
+#: rest_framework/fields.py:1051
msgid "The submitted data was not a file. Check the encoding type on the form."
msgstr ""
-#: rest_framework/fields.py:1049
+#: rest_framework/fields.py:1052
msgid "No filename could be determined."
msgstr ""
-#: rest_framework/fields.py:1050
+#: rest_framework/fields.py:1053
msgid "The submitted file is empty."
msgstr ""
-#: rest_framework/fields.py:1051
+#: rest_framework/fields.py:1054
msgid ""
"Ensure this filename has at most {max_length} characters (it has {length})."
msgstr ""
-#: rest_framework/fields.py:1093
+#: rest_framework/fields.py:1096
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
@@ -276,3 +280,19 @@ msgstr ""
#: rest_framework/validators.py:247
msgid "This field must be unique for the \"{date_field}\" year."
msgstr ""
+
+#: rest_framework/versioning.py:39
+msgid "Invalid version in 'Accept' header."
+msgstr ""
+
+#: rest_framework/versioning.py:70 rest_framework/versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: rest_framework/versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: rest_framework/versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
From 17665aa52a9cd5599099c19fd8f54540a5d436ce Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 12:26:15 +0000
Subject: [PATCH 040/121] Add docs for OAuth, XML, YAML, JSONP packages. Closes
#2179.
---
README.md | 4 +-
docs/api-guide/authentication.md | 47 +++++++++++++++++--
docs/api-guide/parsers.md | 51 ++++++++++++++++++--
docs/api-guide/renderers.md | 80 +++++++++++++++++++++++++++++++-
docs/index.md | 9 ++--
5 files changed, 174 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 4c9d765ea..6742a7b1d 100644
--- a/README.md
+++ b/README.md
@@ -190,8 +190,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[sandbox]: http://restframework.herokuapp.com/
[index]: http://www.django-rest-framework.org/
-[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication
-[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers
[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer
[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 2074f1bfc..bb7318177 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -293,14 +293,49 @@ The following example will authenticate any incoming request as the user given b
The following third party packages are also available.
+## Django OAuth Toolkit
+
+The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
+
+#### Installation & configuration
+
+Install using `pip`.
+
+ pip install django-oauth-toolkit
+
+Add the package to your `INSTALLED_APPS` and modify your REST framework settings.
+
+ INSTALLED_APPS = (
+ ...
+ 'oauth2_provider',
+ )
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'oauth2_provider.ext.rest_framework.OAuth2Authentication',
+ )
+ }
+
+For more details see the [Django REST framework - Getting started][django-oauth-toolkit-getting-started] documentation.
+
+## Django REST framework OAuth
+
+The [Django REST framework OAuth][django-rest-framework-oauth] package provides both OAuth1 and OAuth2 support for REST framework.
+
+This package was previously included directly in REST framework but is now supported and maintained as a third party package.
+
+#### Installation & configuration
+
+Install the package using `pip`.
+
+ pip install djangorestframework-oauth
+
+For details on configuration and usage see the Django REST framework OAuth documentation for [authentication][django-rest-framework-oauth-authentication] and [permissions][django-rest-framework-oauth-permissions].
+
## Digest Authentication
HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
-## Django OAuth Toolkit
-
-The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
-
## Django OAuth2 Consumer
The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
@@ -332,6 +367,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization
[custom-user-model]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model
[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html
+[django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.org/en/latest/rest-framework/getting_started.html
+[django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/
+[django-rest-framework-oauth-authentication]: http://jpadilla.github.io/django-rest-framework-oauth/authentication/
+[django-rest-framework-oauth-permissions]: http://jpadilla.github.io/django-rest-framework-oauth/permissions/
[juanriaza]: https://github.com/juanriaza
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
[oauth-1.0a]: http://oauth.net/core/1.0a
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 9323d3822..b68b33be9 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -26,7 +26,7 @@ As an example, if you are sending `json` encoded data using jQuery with the [.aj
## Setting the parsers
-The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `JSON` content.
+The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow only requests with `JSON` content, instead of the default of JSON or form data.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
@@ -37,8 +37,8 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE
You can also set the parsers used for an individual view, or viewset,
using the `APIView` class based views.
- from rest_framework.parsers import JSONParser
- from rest_framework.response import Response
+ from rest_framework.parsers import JSONParser
+ from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
@@ -162,6 +162,48 @@ The following is an example plaintext parser that will populate the `request.dat
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
@@ -173,6 +215,9 @@ The following third party packages are also available.
[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
[juanriaza]: https://github.com/juanriaza
[vbabiy]: https://github.com/vbabiy
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 69460dbc5..83ded849d 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -342,13 +342,81 @@ Templates will render with a `RequestContext` which includes the `status_code` a
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
+## JSONP
+
+[REST framework JSONP][rest-framework-jsonp] provides JSONP rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+---
+
+**Warning**: If you require cross-domain AJAX requests, you should generally be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
+
+The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
+
+---
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-jsonp
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.JSONPRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
## CSV
-Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
+Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
## UltraJSON
@@ -358,7 +426,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
-
## Pandas (CSV, Excel, PNG)
[Django REST Pandas] provides a serializer and renderers that support additional data processing and output via the [Pandas] DataFrame API. Django REST Pandas includes renderers for Pandas-style CSV files, Excel workbooks (both `.xls` and `.xlsx`), and a number of [other formats]. It is maintained by [S. Andrew Sheppard][sheppard] as part of the [wq Project][wq].
@@ -373,10 +440,19 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[application/vnd.github+json]: http://developer.github.com/v3/media/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
+[rest-framework-jsonp]: http://jpadilla.github.io/django-rest-framework-jsonp/
+[cors]: http://www.w3.org/TR/cors/
+[cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/
+[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
[messagepack]: http://msgpack.org/
[juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu
[vbabiy]: https://github.com/vbabiy
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
[ultrajson]: https://github.com/esnme/ultrajson
diff --git a/docs/index.md b/docs/index.md
index 7ccec12fe..544204c65 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -28,13 +28,12 @@ For more details see the [3.0 release notes][3.0-announcement].
-
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
Some reasons you might want to use REST framework:
* The [Web browsable API][sandbox] is a huge usability win for your developers.
-* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
+* [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
@@ -57,7 +56,6 @@ The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
-* [django-restframework-oauth][django-restframework-oauth] package for OAuth 1.0a and 2.0 support.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
## Installation
@@ -260,13 +258,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
[django-filter]: http://pypi.python.org/pypi/django-filter
-[django-restframework-oauth]: https://github.com/jlafon/django-rest-framework-oauth
[django-guardian]: https://github.com/lukaszb/django-guardian
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[index]: .
-[oauth1-section]: api-guide/authentication#oauthauthentication
-[oauth2-section]: api-guide/authentication#oauth2authentication
+[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: api-guide/authentication/#django-oauth-toolkit
[serializer-section]: api-guide/serializers#serializers
[modelserializer-section]: api-guide/serializers#modelserializer
[functionview-section]: api-guide/views#function-based-views
From 49dc037a961b618baf8eb189b094633238867b41 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 15:03:09 +0000
Subject: [PATCH 041/121] Update docstring
---
rest_framework/serializers.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 623ed5865..08a584333 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -236,11 +236,11 @@ class BaseSerializer(Field):
class SerializerMetaclass(type):
"""
- This metaclass sets a dictionary named `base_fields` on the class.
+ This metaclass sets a dictionary named `_declared_fields` on the class.
Any instances of `Field` included as attributes on either the class
or on any of its superclasses will be include in the
- `base_fields` dictionary.
+ `_declared_fields` dictionary.
"""
@classmethod
From 7913947757b0e6bd1b8828db81933c32c498e20a Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 11:13:03 +0000
Subject: [PATCH 042/121] add config and documentation about
uploading/downloading translations from Transifex
---
.tx/config | 9 +++++
CONTRIBUTING.md | 54 +++++++++++++++++++++++++++++
docs/topics/internationalisation.md | 11 +++---
3 files changed, 70 insertions(+), 4 deletions(-)
create mode 100644 .tx/config
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 000000000..271fa1e35
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,9 @@
+[main]
+host = https://www.transifex.com
+
+[django-rest-framework.djangopo]
+file_filter = rest_framework/locale//LC_MESSAGES/django.po
+source_file = rest_framework/locale/en_US/LC_MESSAGES/django.po
+source_lang = en_US
+type = PO
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b963a4993..d94eb87e0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -177,6 +177,57 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good
Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
+# Translations
+
+If REST framework isn't translated into your language you can request that it is at the [Transifex project][transifex].
+
+## Managing Transfiex
+The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
+
+```
+pip install transifex-client
+```
+
+To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your authentication information:
+
+```
+[https://www.transifex.com]
+username = user
+token =
+password = p@ssw0rd
+hostname = https://www.transifex.com
+```
+
+## Upload new source translations
+When any user-visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
+
+```
+cd rest_framework
+django-admin.py makemessages -l en_US
+cd ..
+tx push -s
+```
+
+When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
+
+Here's how differences between the old and new source files will be handled:
+
+* New strings will be added.
+* Modified strings will be added as well.
+* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically restore the translated string too.
+
+
+## Get translations
+When a translator has finished translating their work needs to be downloaded from Transifex into the source repo. To do this, run:
+
+```
+tx pull -a
+cd rest_framework
+django-admin.py compilemessages
+```
+
+You can then commit as normal.
+
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
@@ -190,3 +241,6 @@ Once your package is decently documented and available on PyPI open a pull reque
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
+[transifex]: https://www.transifex.com/projects/p/django-rest-framework/
+[transifex-client]: https://pypi.python.org/pypi/transifex-client
+[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
\ No newline at end of file
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
index fac3bdb7a..2a476c864 100644
--- a/docs/topics/internationalisation.md
+++ b/docs/topics/internationalisation.md
@@ -3,12 +3,14 @@ REST framework ships with translatable error messages. You can make these appea
## How to translate REST Framework errors
+REST framework translations are managed online using [Transifex.com][transifex]. To get started, checkout the guide in the [CONTRIBUTING.md guide][contributing].
+
+Sometimes you may want to use REST Framework in a language which has not been translated yet on Transifex. If that is the case then you should translate the error messages locally.
+
+#### How to translate REST Framework error messages locally:
This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
-
-#### To translate REST framework error messages:
-
1. Make a new folder where you want to store the translated errors. Add this
path to your [`LOCALE_PATHS`][django-locale-paths] setting.
@@ -89,4 +91,5 @@ display as Django does. You can find more info in the [Django docs on discoveri
[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
-[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
\ No newline at end of file
+[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
+[contributing]: ../../CONTRIBUTING.md
From 9b4177b6ea38de6e86b0fe723834b6ef36af15b3 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 11:41:06 +0000
Subject: [PATCH 043/121] switch to using format strings in error messages;
raise NotFound when pagination fails to provide a more useful error message
---
rest_framework/exceptions.py | 28 ++++++++++++++--------------
rest_framework/generics.py | 14 ++++++++------
2 files changed, 22 insertions(+), 20 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index c8cedfceb..dfc57293e 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -7,8 +7,7 @@ In addition Django's built in 403 and 404 exceptions are handled.
from __future__ import unicode_literals
from django.utils import six
from django.utils.encoding import force_text
-from django.utils.translation import ugettext_lazy as _
-from django.utils.translation import ungettext_lazy
+from django.utils.translation import ugettext_lazy as _, ungettext
from rest_framework import status
import math
@@ -96,13 +95,13 @@ class NotFound(APIException):
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = _("Method '%s' not allowed.")
+ default_detail = _("Method {method} not allowed.")
def __init__(self, method, detail=None):
if detail is not None:
self.detail = force_text(detail)
else:
- self.detail = force_text(self.default_detail) % method
+ self.detail = force_text(self.default_detail).format(method=method)
class NotAcceptable(APIException):
@@ -119,23 +118,22 @@ class NotAcceptable(APIException):
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = _("Unsupported media type '%s' in request.")
+ default_detail = _("Unsupported media type '{media_type}' in request.")
def __init__(self, media_type, detail=None):
if detail is not None:
self.detail = force_text(detail)
else:
- self.detail = force_text(self.default_detail) % media_type
+ self.detail = force_text(self.default_detail).format(
+ media_type=media_type
+ )
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
default_detail = _('Request was throttled.')
- extra_detail = ungettext_lazy(
- 'Expected available in %(wait)d second.',
- 'Expected available in %(wait)d seconds.',
- 'wait'
- )
+ extra_detail_singular = 'Expected available in {wait} second.'
+ extra_detail_plural = 'Expected available in {wait} seconds.'
def __init__(self, wait=None, detail=None):
if detail is not None:
@@ -147,6 +145,8 @@ class Throttled(APIException):
self.wait = None
else:
self.wait = math.ceil(wait)
- self.detail += ' ' + force_text(
- self.extra_detail % {'wait': self.wait}
- )
+ self.detail += ' ' + force_text(ungettext(
+ self.extra_detail_singular.format(wait=self.wait),
+ self.extra_detail_plural.format(wait=self.wait),
+ self.wait
+ ))
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 680992d75..fe92355d9 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -10,6 +10,7 @@ from django.shortcuts import get_object_or_404 as _get_object_or_404
from django.utils import six
from django.utils.translation import ugettext as _
from rest_framework import views, mixins
+from rest_framework.exceptions import NotFound
from rest_framework.settings import api_settings
@@ -119,15 +120,16 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise Http404(_("Choose a valid page number. Page numbers must be a whole number, or must be the string 'last'."))
+ raise NotFound(_("Choose a valid page number. Page numbers must be a whole number, or must be the string 'last'."))
+
+ page_number = -1
try:
page = paginator.page(page_number)
except InvalidPage as exc:
- error_format = _('Invalid page (%(page_number)s): %(message)s')
- raise Http404(error_format % {
- 'page_number': page_number,
- 'message': six.text_type(exc)
- })
+ error_format = _('Invalid page ({page_number}): {message}')
+ raise NotFound(error_format.format(
+ page_number=page_number, message=six.text_type(exc)
+ ))
return page
From 3819ae35ac70ef25804f285b7b59edf2f67ea915 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 11:42:36 +0000
Subject: [PATCH 044/121] recompile pofile with new python format strings
---
.../locale/en_US/LC_MESSAGES/django.po | 153 ++++++++----------
1 file changed, 69 insertions(+), 84 deletions(-)
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 569020739..7c5a6c02b 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-02 11:10+0000\n"
+"POT-Creation-Date: 2015-01-07 11:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,282 +17,267 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: rest_framework/authtoken/serializers.py:20
+#: authtoken/serializers.py:20
msgid "User account is disabled."
msgstr ""
-#: rest_framework/authtoken/serializers.py:23
+#: authtoken/serializers.py:23
msgid "Unable to log in with provided credentials."
msgstr ""
-#: rest_framework/authtoken/serializers.py:26
+#: authtoken/serializers.py:26
msgid "Must include \"username\" and \"password\""
msgstr ""
-#: rest_framework/exceptions.py:39
+#: exceptions.py:38
msgid "A server error occurred."
msgstr ""
-#: rest_framework/exceptions.py:74
+#: exceptions.py:73
msgid "Malformed request."
msgstr ""
-#: rest_framework/exceptions.py:79
+#: exceptions.py:78
msgid "Incorrect authentication credentials."
msgstr ""
-#: rest_framework/exceptions.py:84
+#: exceptions.py:83
msgid "Authentication credentials were not provided."
msgstr ""
-#: rest_framework/exceptions.py:89
+#: exceptions.py:88
msgid "You do not have permission to perform this action."
msgstr ""
-#: rest_framework/exceptions.py:94
+#: exceptions.py:93
msgid "Not found."
msgstr ""
-#: rest_framework/exceptions.py:99
-#, python-format
-msgid "Method '%s' not allowed."
+#: exceptions.py:98
+msgid "Method {method} not allowed."
msgstr ""
-#: rest_framework/exceptions.py:110
+#: exceptions.py:109
msgid "Could not satisfy the request Accept header."
msgstr ""
-#: rest_framework/exceptions.py:122
-#, python-format
-msgid "Unsupported media type '%s' in request."
+#: exceptions.py:121
+msgid "Unsupported media type '{media_type}' in request."
msgstr ""
-#: rest_framework/exceptions.py:133
+#: exceptions.py:134
msgid "Request was throttled."
msgstr ""
-#: rest_framework/exceptions.py:135
-#, python-format
-msgid "Expected available in %(wait)d second."
-msgid_plural "Expected available in %(wait)d seconds."
-msgstr[0] ""
-msgstr[1] ""
-
-#: rest_framework/fields.py:152 rest_framework/relations.py:131
-#: rest_framework/relations.py:155 rest_framework/validators.py:77
-#: rest_framework/validators.py:155
+#: fields.py:152 relations.py:131 relations.py:155 validators.py:77
+#: validators.py:155
msgid "This field is required."
msgstr ""
-#: rest_framework/fields.py:153
+#: fields.py:153
msgid "This field may not be null."
msgstr ""
-#: rest_framework/fields.py:480 rest_framework/fields.py:508
+#: fields.py:480 fields.py:508
msgid "`{input}` is not a valid boolean."
msgstr ""
-#: rest_framework/fields.py:543
+#: fields.py:543
msgid "This field may not be blank."
msgstr ""
-#: rest_framework/fields.py:544 rest_framework/fields.py:1252
+#: fields.py:544 fields.py:1252
msgid "Ensure this field has no more than {max_length} characters."
msgstr ""
-#: rest_framework/fields.py:545
+#: fields.py:545
msgid "Ensure this field has at least {min_length} characters."
msgstr ""
-#: rest_framework/fields.py:587
+#: fields.py:587
msgid "Enter a valid email address."
msgstr ""
-#: rest_framework/fields.py:604
+#: fields.py:604
msgid "This value does not match the required pattern."
msgstr ""
-#: rest_framework/fields.py:615
+#: fields.py:615
msgid ""
"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
msgstr ""
-#: rest_framework/fields.py:627
+#: fields.py:627
msgid "Enter a valid URL."
msgstr ""
-#: rest_framework/fields.py:640
+#: fields.py:640
msgid "A valid integer is required."
msgstr ""
-#: rest_framework/fields.py:641 rest_framework/fields.py:675
-#: rest_framework/fields.py:708
+#: fields.py:641 fields.py:675 fields.py:708
msgid "Ensure this value is less than or equal to {max_value}."
msgstr ""
-#: rest_framework/fields.py:642 rest_framework/fields.py:676
-#: rest_framework/fields.py:709
+#: fields.py:642 fields.py:676 fields.py:709
msgid "Ensure this value is greater than or equal to {min_value}."
msgstr ""
-#: rest_framework/fields.py:643 rest_framework/fields.py:677
-#: rest_framework/fields.py:713
+#: fields.py:643 fields.py:677 fields.py:713
msgid "String value too large."
msgstr ""
-#: rest_framework/fields.py:674 rest_framework/fields.py:707
+#: fields.py:674 fields.py:707
msgid "A valid number is required."
msgstr ""
-#: rest_framework/fields.py:710
+#: fields.py:710
msgid "Ensure that there are no more than {max_digits} digits in total."
msgstr ""
-#: rest_framework/fields.py:711
+#: fields.py:711
msgid "Ensure that there are no more than {max_decimal_places} decimal places."
msgstr ""
-#: rest_framework/fields.py:712
+#: fields.py:712
msgid ""
"Ensure that there are no more than {max_whole_digits} digits before the "
"decimal point."
msgstr ""
-#: rest_framework/fields.py:796
+#: fields.py:796
msgid "Datetime has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:797
+#: fields.py:797
msgid "Expected a datetime but got a date."
msgstr ""
-#: rest_framework/fields.py:861
+#: fields.py:861
msgid "Date has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:862
+#: fields.py:862
msgid "Expected a date but got a datetime."
msgstr ""
-#: rest_framework/fields.py:919
+#: fields.py:919
msgid "Time has wrong format. Use one of these formats instead: {format}."
msgstr ""
-#: rest_framework/fields.py:975 rest_framework/fields.py:1019
+#: fields.py:975 fields.py:1019
msgid "`{input}` is not a valid choice."
msgstr ""
-#: rest_framework/fields.py:1020 rest_framework/fields.py:1121
-#: rest_framework/serializers.py:476
+#: fields.py:1020 fields.py:1121 serializers.py:476
msgid "Expected a list of items but got type `{input_type}`."
msgstr ""
-#: rest_framework/fields.py:1050
+#: fields.py:1050
msgid "No file was submitted."
msgstr ""
-#: rest_framework/fields.py:1051
+#: fields.py:1051
msgid "The submitted data was not a file. Check the encoding type on the form."
msgstr ""
-#: rest_framework/fields.py:1052
+#: fields.py:1052
msgid "No filename could be determined."
msgstr ""
-#: rest_framework/fields.py:1053
+#: fields.py:1053
msgid "The submitted file is empty."
msgstr ""
-#: rest_framework/fields.py:1054
+#: fields.py:1054
msgid ""
"Ensure this filename has at most {max_length} characters (it has {length})."
msgstr ""
-#: rest_framework/fields.py:1096
+#: fields.py:1096
msgid ""
"Upload a valid image. The file you uploaded was either not an image or a "
"corrupted image."
msgstr ""
-#: rest_framework/generics.py:122
+#: generics.py:123
msgid ""
"Choose a valid page number. Page numbers must be a whole number, or must be "
"the string 'last'."
msgstr ""
-#: rest_framework/generics.py:126
-#, python-format
-msgid "Invalid page (%(page_number)s): %(message)s"
+#: generics.py:129
+msgid "Invalid page ({page_number}): {message}"
msgstr ""
-#: rest_framework/relations.py:132
+#: relations.py:132
msgid "Invalid pk '{pk_value}' - object does not exist."
msgstr ""
-#: rest_framework/relations.py:133
+#: relations.py:133
msgid "Incorrect type. Expected pk value, received {data_type}."
msgstr ""
-#: rest_framework/relations.py:156
+#: relations.py:156
msgid "Invalid hyperlink - No URL match"
msgstr ""
-#: rest_framework/relations.py:157
+#: relations.py:157
msgid "Invalid hyperlink - Incorrect URL match."
msgstr ""
-#: rest_framework/relations.py:158
+#: relations.py:158
msgid "Invalid hyperlink - Object does not exist."
msgstr ""
-#: rest_framework/relations.py:159
+#: relations.py:159
msgid "Incorrect type. Expected URL string, received {data_type}."
msgstr ""
-#: rest_framework/relations.py:294
+#: relations.py:294
msgid "Object with {slug_name}={value} does not exist."
msgstr ""
-#: rest_framework/relations.py:295
+#: relations.py:295
msgid "Invalid value."
msgstr ""
-#: rest_framework/serializers.py:299
+#: serializers.py:299
msgid "Invalid data. Expected a dictionary, but got {datatype}."
msgstr ""
-#: rest_framework/validators.py:22
+#: validators.py:22
msgid "This field must be unique."
msgstr ""
-#: rest_framework/validators.py:76
+#: validators.py:76
msgid "The fields {field_names} must make a unique set."
msgstr ""
-#: rest_framework/validators.py:219
+#: validators.py:219
msgid "This field must be unique for the \"{date_field}\" date."
msgstr ""
-#: rest_framework/validators.py:234
+#: validators.py:234
msgid "This field must be unique for the \"{date_field}\" month."
msgstr ""
-#: rest_framework/validators.py:247
+#: validators.py:247
msgid "This field must be unique for the \"{date_field}\" year."
msgstr ""
-#: rest_framework/versioning.py:39
+#: versioning.py:39
msgid "Invalid version in 'Accept' header."
msgstr ""
-#: rest_framework/versioning.py:70 rest_framework/versioning.py:112
+#: versioning.py:70 versioning.py:112
msgid "Invalid version in URL path."
msgstr ""
-#: rest_framework/versioning.py:138
+#: versioning.py:138
msgid "Invalid version in hostname."
msgstr ""
-#: rest_framework/versioning.py:160
+#: versioning.py:160
msgid "Invalid version in query parameter."
msgstr ""
From fe5d93c8cbc5f3a9b1b6715208c70f485be68bdf Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 11:44:18 +0000
Subject: [PATCH 045/121] remove hardcoded page number
---
rest_framework/generics.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index fe92355d9..7c4d5e958 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -122,7 +122,6 @@ class GenericAPIView(views.APIView):
else:
raise NotFound(_("Choose a valid page number. Page numbers must be a whole number, or must be the string 'last'."))
- page_number = -1
try:
page = paginator.page(page_number)
except InvalidPage as exc:
From 4c32083b8b59a50877633910055313dad7bb117e Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 12:01:11 +0000
Subject: [PATCH 046/121] use double quotes for user visible strings; end user
visible strings in full stops; add some missing translation tags
---
rest_framework/authentication.py | 17 ++++---
rest_framework/authtoken/serializers.py | 6 +--
rest_framework/exceptions.py | 24 ++++-----
rest_framework/fields.py | 68 ++++++++++++-------------
rest_framework/generics.py | 2 +-
rest_framework/relations.py | 16 +++---
rest_framework/serializers.py | 4 +-
rest_framework/validators.py | 14 ++---
rest_framework/versioning.py | 8 +--
9 files changed, 80 insertions(+), 79 deletions(-)
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index 124ef68ac..7e86a7b9f 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
import base64
from django.contrib.auth import authenticate
from django.middleware.csrf import CsrfViewMiddleware
+from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.authtoken.models import Token
@@ -65,16 +66,16 @@ class BasicAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid basic header. No credentials provided.'
+ msg = _("Invalid basic header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid basic header. Credentials string should not contain spaces.'
+ msg = _("Invalid basic header. Credentials string should not contain spaces.")
raise exceptions.AuthenticationFailed(msg)
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError):
- msg = 'Invalid basic header. Credentials not correctly base64 encoded'
+ msg = _("Invalid basic header. Credentials not correctly base64 encoded.")
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
@@ -86,7 +87,7 @@ class BasicAuthentication(BaseAuthentication):
"""
user = authenticate(username=userid, password=password)
if user is None or not user.is_active:
- raise exceptions.AuthenticationFailed('Invalid username/password')
+ raise exceptions.AuthenticationFailed(_("Invalid username/password."))
return (user, None)
def authenticate_header(self, request):
@@ -152,10 +153,10 @@ class TokenAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid token header. No credentials provided.'
+ msg = _("Invalid token header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid token header. Token string should not contain spaces.'
+ msg = _("Invalid token header. Token string should not contain spaces.")
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
@@ -164,10 +165,10 @@ class TokenAuthentication(BaseAuthentication):
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
+ raise exceptions.AuthenticationFailed(_("Invalid token"))
if not token.user.is_active:
- raise exceptions.AuthenticationFailed('User inactive or deleted')
+ raise exceptions.AuthenticationFailed(_("User inactive or deleted"))
return (token.user, token)
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index f31dded17..78fe6a117 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -17,13 +17,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user:
if not user.is_active:
- msg = _('User account is disabled.')
+ msg = _("User account is disabled.")
raise exceptions.ValidationError(msg)
else:
- msg = _('Unable to log in with provided credentials.')
+ msg = _("Unable to log in with provided credentials.")
raise exceptions.ValidationError(msg)
else:
- msg = _('Must include "username" and "password"')
+ msg = _("Must include \"username\" and \"password\"")
raise exceptions.ValidationError(msg)
attrs['user'] = user
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index dfc57293e..3ca8e5380 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -35,7 +35,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = _('A server error occurred.')
+ default_detail = _("A server error occurred.")
def __init__(self, detail=None):
if detail is not None:
@@ -52,7 +52,7 @@ class APIException(Exception):
# built in `ValidationError`. For example:
#
# from rest_framework import serializers
-# raise serializers.ValidationError('Value was invalid')
+# raise serializers.ValidationError("Value was invalid")
class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
@@ -70,32 +70,32 @@ class ValidationError(APIException):
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = _('Malformed request.')
+ default_detail = _("Malformed request.")
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = _('Incorrect authentication credentials.')
+ default_detail = _("Incorrect authentication credentials.")
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = _('Authentication credentials were not provided.')
+ default_detail = _("Authentication credentials were not provided.")
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = _('You do not have permission to perform this action.')
+ default_detail = _("You do not have permission to perform this action.")
class NotFound(APIException):
status_code = status.HTTP_404_NOT_FOUND
- default_detail = _('Not found.')
+ default_detail = _("Not found.")
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = _("Method {method} not allowed.")
+ default_detail = _("Method '{method}' not allowed.")
def __init__(self, method, detail=None):
if detail is not None:
@@ -106,7 +106,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = _('Could not satisfy the request Accept header.')
+ default_detail = _("Could not satisfy the request Accept header.")
def __init__(self, detail=None, available_renderers=None):
if detail is not None:
@@ -131,9 +131,9 @@ class UnsupportedMediaType(APIException):
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = _('Request was throttled.')
- extra_detail_singular = 'Expected available in {wait} second.'
- extra_detail_plural = 'Expected available in {wait} seconds.'
+ default_detail = _("Request was throttled.")
+ extra_detail_singular = "Expected available in {wait} second."
+ extra_detail_plural = "Expected available in {wait} seconds."
def __init__(self, wait=None, detail=None):
if detail is not None:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 0ff2b0733..8a781b356 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -149,8 +149,8 @@ class Field(object):
_creation_counter = 0
default_error_messages = {
- 'required': _('This field is required.'),
- 'null': _('This field may not be null.')
+ 'required': _("This field is required."),
+ 'null': _("This field may not be null.")
}
default_validators = []
default_empty_html = empty
@@ -477,7 +477,7 @@ class Field(object):
class BooleanField(Field):
default_error_messages = {
- 'invalid': _('`{input}` is not a valid boolean.')
+ 'invalid': _("`{input}` is not a valid boolean.")
}
default_empty_html = False
initial = False
@@ -505,7 +505,7 @@ class BooleanField(Field):
class NullBooleanField(Field):
default_error_messages = {
- 'invalid': _('`{input}` is not a valid boolean.')
+ 'invalid': _("`{input}` is not a valid boolean.")
}
initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
@@ -540,9 +540,9 @@ class NullBooleanField(Field):
class CharField(Field):
default_error_messages = {
- 'blank': _('This field may not be blank.'),
- 'max_length': _('Ensure this field has no more than {max_length} characters.'),
- 'min_length': _('Ensure this field has at least {min_length} characters.')
+ 'blank': _("This field may not be blank."),
+ 'max_length': _("Ensure this field has no more than {max_length} characters."),
+ 'min_length': _("Ensure this field has at least {min_length} characters.")
}
initial = ''
coerce_blank_to_null = False
@@ -584,7 +584,7 @@ class CharField(Field):
class EmailField(CharField):
default_error_messages = {
- 'invalid': _('Enter a valid email address.')
+ 'invalid': _("Enter a valid email address.")
}
def __init__(self, **kwargs):
@@ -601,7 +601,7 @@ class EmailField(CharField):
class RegexField(CharField):
default_error_messages = {
- 'invalid': _('This value does not match the required pattern.')
+ 'invalid': _("This value does not match the required pattern.")
}
def __init__(self, regex, **kwargs):
@@ -637,10 +637,10 @@ class URLField(CharField):
class IntegerField(Field):
default_error_messages = {
- 'invalid': _('A valid integer is required.'),
- 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
- 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
- 'max_string_length': _('String value too large.')
+ 'invalid': _("A valid integer is required."),
+ 'max_value': _("Ensure this value is less than or equal to {max_value}."),
+ 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
+ 'max_string_length': _("String value too large.")
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -672,9 +672,9 @@ class IntegerField(Field):
class FloatField(Field):
default_error_messages = {
'invalid': _("A valid number is required."),
- 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
- 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
- 'max_string_length': _('String value too large.')
+ 'max_value': _("Ensure this value is less than or equal to {max_value}."),
+ 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
+ 'max_string_length': _("String value too large.")
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -704,13 +704,13 @@ class FloatField(Field):
class DecimalField(Field):
default_error_messages = {
- 'invalid': _('A valid number is required.'),
- 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
- 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
- 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
- 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
- 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'),
- 'max_string_length': _('String value too large.')
+ 'invalid': _("A valid number is required."),
+ 'max_value': _("Ensure this value is less than or equal to {max_value}."),
+ 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
+ 'max_digits': _("Ensure that there are no more than {max_digits} digits in total."),
+ 'max_decimal_places': _("Ensure that there are no more than {max_decimal_places} decimal places."),
+ 'max_whole_digits': _("Ensure that there are no more than {max_whole_digits} digits before the decimal point."),
+ 'max_string_length': _("String value too large.")
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -793,8 +793,8 @@ class DecimalField(Field):
class DateTimeField(Field):
default_error_messages = {
- 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
- 'date': _('Expected a datetime but got a date.'),
+ 'invalid': _("Datetime has wrong format. Use one of these formats instead: {format}."),
+ 'date': _("Expected a datetime but got a date."),
}
format = api_settings.DATETIME_FORMAT
input_formats = api_settings.DATETIME_INPUT_FORMATS
@@ -858,8 +858,8 @@ class DateTimeField(Field):
class DateField(Field):
default_error_messages = {
- 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
- 'datetime': _('Expected a date but got a datetime.'),
+ 'invalid': _("Date has wrong format. Use one of these formats instead: {format}."),
+ 'datetime': _("Expected a date but got a datetime."),
}
format = api_settings.DATE_FORMAT
input_formats = api_settings.DATE_INPUT_FORMATS
@@ -916,7 +916,7 @@ class DateField(Field):
class TimeField(Field):
default_error_messages = {
- 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
+ 'invalid': _("Time has wrong format. Use one of these formats instead: {format}."),
}
format = api_settings.TIME_FORMAT
input_formats = api_settings.TIME_INPUT_FORMATS
@@ -972,7 +972,7 @@ class TimeField(Field):
class ChoiceField(Field):
default_error_messages = {
- 'invalid_choice': _('`{input}` is not a valid choice.')
+ 'invalid_choice': _("`{input}` is not a valid choice.")
}
def __init__(self, choices, **kwargs):
@@ -1016,8 +1016,8 @@ class ChoiceField(Field):
class MultipleChoiceField(ChoiceField):
default_error_messages = {
- 'invalid_choice': _('`{input}` is not a valid choice.'),
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'invalid_choice': _("`{input}` is not a valid choice."),
+ 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
}
default_empty_html = []
@@ -1051,7 +1051,7 @@ class FileField(Field):
'invalid': _("The submitted data was not a file. Check the encoding type on the form."),
'no_name': _("No filename could be determined."),
'empty': _("The submitted file is empty."),
- 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
+ 'max_length': _("Ensure this filename has at most {max_length} characters (it has {length})."),
}
use_url = api_settings.UPLOADED_FILES_USE_URL
@@ -1118,7 +1118,7 @@ class ListField(Field):
child = None
initial = []
default_error_messages = {
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
}
def __init__(self, *args, **kwargs):
@@ -1249,7 +1249,7 @@ class ModelField(Field):
that do not have a serializer field to be mapped to.
"""
default_error_messages = {
- 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ 'max_length': _("Ensure this field has no more than {max_length} characters."),
}
def __init__(self, model_field, **kwargs):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7c4d5e958..c7053d8f3 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -125,7 +125,7 @@ class GenericAPIView(views.APIView):
try:
page = paginator.page(page_number)
except InvalidPage as exc:
- error_format = _('Invalid page ({page_number}): {message}')
+ error_format = _("Invalid page ({page_number}): {message}.")
raise NotFound(error_format.format(
page_number=page_number, message=six.text_type(exc)
))
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 7b119291d..3737b21fa 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -128,9 +128,9 @@ class StringRelatedField(RelatedField):
class PrimaryKeyRelatedField(RelatedField):
default_error_messages = {
- 'required': _('This field is required.'),
+ 'required': _("This field is required."),
'does_not_exist': _("Invalid pk '{pk_value}' - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
+ 'incorrect_type': _("Incorrect type. Expected pk value, received {data_type}."),
}
def use_pk_only_optimization(self):
@@ -152,11 +152,11 @@ class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
default_error_messages = {
- 'required': _('This field is required.'),
- 'no_match': _('Invalid hyperlink - No URL match'),
- 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
- 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
- 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
+ 'required': _("This field is required."),
+ 'no_match': _("Invalid hyperlink - No URL match."),
+ 'incorrect_match': _("Invalid hyperlink - Incorrect URL match."),
+ 'does_not_exist': _("Invalid hyperlink - Object does not exist."),
+ 'incorrect_type': _("Incorrect type. Expected URL string, received {data_type}."),
}
def __init__(self, view_name=None, **kwargs):
@@ -292,7 +292,7 @@ class SlugRelatedField(RelatedField):
default_error_messages = {
'does_not_exist': _("Object with {slug_name}={value} does not exist."),
- 'invalid': _('Invalid value.'),
+ 'invalid': _("Invalid value."),
}
def __init__(self, slug_field=None, **kwargs):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 623ed5865..9d7c8884a 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -296,7 +296,7 @@ def get_validation_error_detail(exc):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
default_error_messages = {
- 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
+ 'invalid': _("Invalid data. Expected a dictionary, but got {datatype}.")
}
@property
@@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer):
many = True
default_error_messages = {
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
}
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index e3719b8d5..cf6f07180 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -19,7 +19,7 @@ class UniqueValidator:
Should be applied to an individual field on the serializer.
"""
- message = _('This field must be unique.')
+ message = _("This field must be unique.")
def __init__(self, queryset, message=None):
self.queryset = queryset
@@ -73,8 +73,8 @@ class UniqueTogetherValidator:
Should be applied to the serializer class, not to an individual field.
"""
- message = _('The fields {field_names} must make a unique set.')
- missing_message = _('This field is required.')
+ message = _("The fields {field_names} must make a unique set.")
+ missing_message = _("This field is required.")
def __init__(self, queryset, fields, message=None):
self.queryset = queryset
@@ -152,7 +152,7 @@ class UniqueTogetherValidator:
class BaseUniqueForValidator:
message = None
- missing_message = _('This field is required.')
+ missing_message = _("This field is required.")
def __init__(self, queryset, field, date_field, message=None):
self.queryset = queryset
@@ -216,7 +216,7 @@ class BaseUniqueForValidator:
class UniqueForDateValidator(BaseUniqueForValidator):
- message = _('This field must be unique for the "{date_field}" date.')
+ message = _("This field must be unique for the \"{date_field}\" date.")
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
@@ -231,7 +231,7 @@ class UniqueForDateValidator(BaseUniqueForValidator):
class UniqueForMonthValidator(BaseUniqueForValidator):
- message = _('This field must be unique for the "{date_field}" month.')
+ message = _("This field must be unique for the \"{date_field}\" month.")
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
@@ -244,7 +244,7 @@ class UniqueForMonthValidator(BaseUniqueForValidator):
class UniqueForYearValidator(BaseUniqueForValidator):
- message = _('This field must be unique for the "{date_field}" year.')
+ message = _("This field must be unique for the \"{date_field}\" year.")
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 440efd139..587ba9f13 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -67,7 +67,7 @@ class URLPathVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _('Invalid version in URL path.')
+ invalid_version_message = _("Invalid version in URL path.")
def determine_version(self, request, *args, **kwargs):
version = kwargs.get(self.version_param, self.default_version)
@@ -109,7 +109,7 @@ class NamespaceVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _('Invalid version in URL path.')
+ invalid_version_message = _("Invalid version in URL path.")
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
@@ -135,7 +135,7 @@ class HostNameVersioning(BaseVersioning):
Accept: application/json
"""
hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
- invalid_version_message = _('Invalid version in hostname.')
+ invalid_version_message = _("Invalid version in hostname.")
def determine_version(self, request, *args, **kwargs):
hostname, seperator, port = request.get_host().partition(':')
@@ -157,7 +157,7 @@ class QueryParameterVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _('Invalid version in query parameter.')
+ invalid_version_message = _("Invalid version in query parameter.")
def determine_version(self, request, *args, **kwargs):
version = request.query_params.get(self.version_param)
From 662a907bdf821c29b42b60ce2b44eb8149a85bd7 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 12:02:04 +0000
Subject: [PATCH 047/121] update source strings
---
.../locale/en_US/LC_MESSAGES/django.po | 42 ++++++++++++++++---
1 file changed, 37 insertions(+), 5 deletions(-)
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 7c5a6c02b..5d0d3a045 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-07 11:40+0000\n"
+"POT-Creation-Date: 2015-01-07 11:58+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -17,6 +17,38 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token"
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted"
+msgstr ""
+
#: authtoken/serializers.py:20
msgid "User account is disabled."
msgstr ""
@@ -54,7 +86,7 @@ msgid "Not found."
msgstr ""
#: exceptions.py:98
-msgid "Method {method} not allowed."
+msgid "Method '{method}' not allowed."
msgstr ""
#: exceptions.py:109
@@ -206,8 +238,8 @@ msgid ""
"the string 'last'."
msgstr ""
-#: generics.py:129
-msgid "Invalid page ({page_number}): {message}"
+#: generics.py:128
+msgid "Invalid page ({page_number}): {message}."
msgstr ""
#: relations.py:132
@@ -219,7 +251,7 @@ msgid "Incorrect type. Expected pk value, received {data_type}."
msgstr ""
#: relations.py:156
-msgid "Invalid hyperlink - No URL match"
+msgid "Invalid hyperlink - No URL match."
msgstr ""
#: relations.py:157
From 9a4267049ba37883e3e0c21b5d453b9551343b8d Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 12:33:37 +0000
Subject: [PATCH 048/121] use double quotes in user messages
---
rest_framework/exceptions.py | 4 ++--
rest_framework/fields.py | 2 +-
rest_framework/generics.py | 4 ++--
rest_framework/relations.py | 2 +-
rest_framework/versioning.py | 2 +-
5 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 3ca8e5380..f8a43871b 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -95,7 +95,7 @@ class NotFound(APIException):
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = _("Method '{method}' not allowed.")
+ default_detail = _("Method \"{method}\" not allowed.")
def __init__(self, method, detail=None):
if detail is not None:
@@ -118,7 +118,7 @@ class NotAcceptable(APIException):
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = _("Unsupported media type '{media_type}' in request.")
+ default_detail = _("Unsupported media type \"{media_type}\" in request.")
def __init__(self, media_type, detail=None):
if detail is not None:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 8a781b356..279446088 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -612,7 +612,7 @@ class RegexField(CharField):
class SlugField(CharField):
default_error_messages = {
- 'invalid': _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
+ 'invalid': _("Enter a valid \"slug\" consisting of letters, numbers, underscores or hyphens.")
}
def __init__(self, **kwargs):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index c7053d8f3..738ba544a 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -120,12 +120,12 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise NotFound(_("Choose a valid page number. Page numbers must be a whole number, or must be the string 'last'."))
+ raise NotFound(_("Choose a valid page number. Page numbers must be a whole number, or must be the string \"last\"."))
try:
page = paginator.page(page_number)
except InvalidPage as exc:
- error_format = _("Invalid page ({page_number}): {message}.")
+ error_format = _("Invalid page \"{page_number}\": {message}.")
raise NotFound(error_format.format(
page_number=page_number, message=six.text_type(exc)
))
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 3737b21fa..42b624e7d 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -129,7 +129,7 @@ class StringRelatedField(RelatedField):
class PrimaryKeyRelatedField(RelatedField):
default_error_messages = {
'required': _("This field is required."),
- 'does_not_exist': _("Invalid pk '{pk_value}' - object does not exist."),
+ 'does_not_exist': _("Invalid pk \"{pk_value}\" - object does not exist."),
'incorrect_type': _("Incorrect type. Expected pk value, received {data_type}."),
}
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 587ba9f13..819c32df8 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -36,7 +36,7 @@ class AcceptHeaderVersioning(BaseVersioning):
Host: example.com
Accept: application/json; version=1.0
"""
- invalid_version_message = _("Invalid version in 'Accept' header.")
+ invalid_version_message = _("Invalid version in \"Accept\" header.")
def determine_version(self, request, *args, **kwargs):
media_type = _MediaType(request.accepted_media_type)
From 91e316f7810157474d6246cd0024bd7f7cc31ff7 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 12:46:23 +0000
Subject: [PATCH 049/121] prefer single quotes in source and double quotes in
user visible strings; add some missing full stops to user visible strings
---
rest_framework/authentication.py | 16 ++--
rest_framework/authtoken/serializers.py | 6 +-
rest_framework/exceptions.py | 24 +++---
rest_framework/fields.py | 82 +++++++++----------
rest_framework/generics.py | 4 +-
.../locale/en_US/LC_MESSAGES/django.po | 23 +++---
rest_framework/relations.py | 20 ++---
rest_framework/serializers.py | 4 +-
rest_framework/validators.py | 14 ++--
rest_framework/versioning.py | 10 +--
tests/test_fields.py | 2 +-
tests/test_generics.py | 6 +-
tests/test_relations.py | 2 +-
13 files changed, 107 insertions(+), 106 deletions(-)
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index 7e86a7b9f..11db05855 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -66,16 +66,16 @@ class BasicAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = _("Invalid basic header. No credentials provided.")
+ msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = _("Invalid basic header. Credentials string should not contain spaces.")
+ msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError):
- msg = _("Invalid basic header. Credentials not correctly base64 encoded.")
+ msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
@@ -87,7 +87,7 @@ class BasicAuthentication(BaseAuthentication):
"""
user = authenticate(username=userid, password=password)
if user is None or not user.is_active:
- raise exceptions.AuthenticationFailed(_("Invalid username/password."))
+ raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
return (user, None)
def authenticate_header(self, request):
@@ -153,10 +153,10 @@ class TokenAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = _("Invalid token header. No credentials provided.")
+ msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = _("Invalid token header. Token string should not contain spaces.")
+ msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
@@ -165,10 +165,10 @@ class TokenAuthentication(BaseAuthentication):
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
- raise exceptions.AuthenticationFailed(_("Invalid token"))
+ raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
- raise exceptions.AuthenticationFailed(_("User inactive or deleted"))
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 78fe6a117..37ade255d 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -17,13 +17,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user:
if not user.is_active:
- msg = _("User account is disabled.")
+ msg = _('User account is disabled.')
raise exceptions.ValidationError(msg)
else:
- msg = _("Unable to log in with provided credentials.")
+ msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg)
else:
- msg = _("Must include \"username\" and \"password\"")
+ msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg)
attrs['user'] = user
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index f8a43871b..f62c9fe39 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -35,7 +35,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = _("A server error occurred.")
+ default_detail = _('A server error occurred.')
def __init__(self, detail=None):
if detail is not None:
@@ -70,32 +70,32 @@ class ValidationError(APIException):
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = _("Malformed request.")
+ default_detail = _('Malformed request.')
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = _("Incorrect authentication credentials.")
+ default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = _("Authentication credentials were not provided.")
+ default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = _("You do not have permission to perform this action.")
+ default_detail = _('You do not have permission to perform this action.')
class NotFound(APIException):
status_code = status.HTTP_404_NOT_FOUND
- default_detail = _("Not found.")
+ default_detail = _('Not found.')
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = _("Method \"{method}\" not allowed.")
+ default_detail = _('Method "{method}" not allowed.')
def __init__(self, method, detail=None):
if detail is not None:
@@ -106,7 +106,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = _("Could not satisfy the request Accept header.")
+ default_detail = _('Could not satisfy the request Accept header.')
def __init__(self, detail=None, available_renderers=None):
if detail is not None:
@@ -118,7 +118,7 @@ class NotAcceptable(APIException):
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = _("Unsupported media type \"{media_type}\" in request.")
+ default_detail = _('Unsupported media type "{media_type}" in request.')
def __init__(self, media_type, detail=None):
if detail is not None:
@@ -131,9 +131,9 @@ class UnsupportedMediaType(APIException):
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = _("Request was throttled.")
- extra_detail_singular = "Expected available in {wait} second."
- extra_detail_plural = "Expected available in {wait} seconds."
+ default_detail = _('Request was throttled.')
+ extra_detail_singular = 'Expected available in {wait} second.'
+ extra_detail_plural = 'Expected available in {wait} seconds.'
def __init__(self, wait=None, detail=None):
if detail is not None:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 279446088..76101608e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -149,8 +149,8 @@ class Field(object):
_creation_counter = 0
default_error_messages = {
- 'required': _("This field is required."),
- 'null': _("This field may not be null.")
+ 'required': _('This field is required.'),
+ 'null': _('This field may not be null.')
}
default_validators = []
default_empty_html = empty
@@ -477,7 +477,7 @@ class Field(object):
class BooleanField(Field):
default_error_messages = {
- 'invalid': _("`{input}` is not a valid boolean.")
+ 'invalid': _('`{input}` is not a valid boolean.')
}
default_empty_html = False
initial = False
@@ -505,7 +505,7 @@ class BooleanField(Field):
class NullBooleanField(Field):
default_error_messages = {
- 'invalid': _("`{input}` is not a valid boolean.")
+ 'invalid': _('`{input}` is not a valid boolean.')
}
initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
@@ -540,9 +540,9 @@ class NullBooleanField(Field):
class CharField(Field):
default_error_messages = {
- 'blank': _("This field may not be blank."),
- 'max_length': _("Ensure this field has no more than {max_length} characters."),
- 'min_length': _("Ensure this field has at least {min_length} characters.")
+ 'blank': _('This field may not be blank.'),
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ 'min_length': _('Ensure this field has at least {min_length} characters.')
}
initial = ''
coerce_blank_to_null = False
@@ -584,7 +584,7 @@ class CharField(Field):
class EmailField(CharField):
default_error_messages = {
- 'invalid': _("Enter a valid email address.")
+ 'invalid': _('Enter a valid email address.')
}
def __init__(self, **kwargs):
@@ -601,7 +601,7 @@ class EmailField(CharField):
class RegexField(CharField):
default_error_messages = {
- 'invalid': _("This value does not match the required pattern.")
+ 'invalid': _('This value does not match the required pattern.')
}
def __init__(self, regex, **kwargs):
@@ -612,7 +612,7 @@ class RegexField(CharField):
class SlugField(CharField):
default_error_messages = {
- 'invalid': _("Enter a valid \"slug\" consisting of letters, numbers, underscores or hyphens.")
+ 'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')
}
def __init__(self, **kwargs):
@@ -624,7 +624,7 @@ class SlugField(CharField):
class URLField(CharField):
default_error_messages = {
- 'invalid': _("Enter a valid URL.")
+ 'invalid': _('Enter a valid URL.')
}
def __init__(self, **kwargs):
@@ -637,10 +637,10 @@ class URLField(CharField):
class IntegerField(Field):
default_error_messages = {
- 'invalid': _("A valid integer is required."),
- 'max_value': _("Ensure this value is less than or equal to {max_value}."),
- 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
- 'max_string_length': _("String value too large.")
+ 'invalid': _('A valid integer is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -671,10 +671,10 @@ class IntegerField(Field):
class FloatField(Field):
default_error_messages = {
- 'invalid': _("A valid number is required."),
- 'max_value': _("Ensure this value is less than or equal to {max_value}."),
- 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
- 'max_string_length': _("String value too large.")
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -704,13 +704,13 @@ class FloatField(Field):
class DecimalField(Field):
default_error_messages = {
- 'invalid': _("A valid number is required."),
- 'max_value': _("Ensure this value is less than or equal to {max_value}."),
- 'min_value': _("Ensure this value is greater than or equal to {min_value}."),
- 'max_digits': _("Ensure that there are no more than {max_digits} digits in total."),
- 'max_decimal_places': _("Ensure that there are no more than {max_decimal_places} decimal places."),
- 'max_whole_digits': _("Ensure that there are no more than {max_whole_digits} digits before the decimal point."),
- 'max_string_length': _("String value too large.")
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
+ 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
+ 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'),
+ 'max_string_length': _('String value too large.')
}
MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
@@ -793,8 +793,8 @@ class DecimalField(Field):
class DateTimeField(Field):
default_error_messages = {
- 'invalid': _("Datetime has wrong format. Use one of these formats instead: {format}."),
- 'date': _("Expected a datetime but got a date."),
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
+ 'date': _('Expected a datetime but got a date.'),
}
format = api_settings.DATETIME_FORMAT
input_formats = api_settings.DATETIME_INPUT_FORMATS
@@ -858,8 +858,8 @@ class DateTimeField(Field):
class DateField(Field):
default_error_messages = {
- 'invalid': _("Date has wrong format. Use one of these formats instead: {format}."),
- 'datetime': _("Expected a date but got a datetime."),
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
+ 'datetime': _('Expected a date but got a datetime.'),
}
format = api_settings.DATE_FORMAT
input_formats = api_settings.DATE_INPUT_FORMATS
@@ -916,7 +916,7 @@ class DateField(Field):
class TimeField(Field):
default_error_messages = {
- 'invalid': _("Time has wrong format. Use one of these formats instead: {format}."),
+ 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
}
format = api_settings.TIME_FORMAT
input_formats = api_settings.TIME_INPUT_FORMATS
@@ -972,7 +972,7 @@ class TimeField(Field):
class ChoiceField(Field):
default_error_messages = {
- 'invalid_choice': _("`{input}` is not a valid choice.")
+ 'invalid_choice': _('`{input}` is not a valid choice.')
}
def __init__(self, choices, **kwargs):
@@ -1016,8 +1016,8 @@ class ChoiceField(Field):
class MultipleChoiceField(ChoiceField):
default_error_messages = {
- 'invalid_choice': _("`{input}` is not a valid choice."),
- 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
+ 'invalid_choice': _('`{input}` is not a valid choice.'),
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
}
default_empty_html = []
@@ -1047,11 +1047,11 @@ class MultipleChoiceField(ChoiceField):
class FileField(Field):
default_error_messages = {
- 'required': _("No file was submitted."),
- 'invalid': _("The submitted data was not a file. Check the encoding type on the form."),
- 'no_name': _("No filename could be determined."),
- 'empty': _("The submitted file is empty."),
- 'max_length': _("Ensure this filename has at most {max_length} characters (it has {length})."),
+ 'required': _('No file was submitted.'),
+ 'invalid': _('The submitted data was not a file. Check the encoding type on the form.'),
+ 'no_name': _('No filename could be determined.'),
+ 'empty': _('The submitted file is empty.'),
+ 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
}
use_url = api_settings.UPLOADED_FILES_USE_URL
@@ -1118,7 +1118,7 @@ class ListField(Field):
child = None
initial = []
default_error_messages = {
- 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
}
def __init__(self, *args, **kwargs):
@@ -1249,7 +1249,7 @@ class ModelField(Field):
that do not have a serializer field to be mapped to.
"""
default_error_messages = {
- 'max_length': _("Ensure this field has no more than {max_length} characters."),
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
}
def __init__(self, model_field, **kwargs):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 738ba544a..7ebed0327 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -120,12 +120,12 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise NotFound(_("Choose a valid page number. Page numbers must be a whole number, or must be the string \"last\"."))
+ raise NotFound(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".'))
try:
page = paginator.page(page_number)
except InvalidPage as exc:
- error_format = _("Invalid page \"{page_number}\": {message}.")
+ error_format = _('Invalid page "{page_number}": {message}.')
raise NotFound(error_format.format(
page_number=page_number, message=six.text_type(exc)
))
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index 5d0d3a045..c8fc7f4d7 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-07 11:58+0000\n"
+"POT-Creation-Date: 2015-01-07 12:28+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -42,11 +42,11 @@ msgid "Invalid token header. Token string should not contain spaces."
msgstr ""
#: authentication.py:168
-msgid "Invalid token"
+msgid "Invalid token."
msgstr ""
#: authentication.py:171
-msgid "User inactive or deleted"
+msgid "User inactive or deleted."
msgstr ""
#: authtoken/serializers.py:20
@@ -58,7 +58,7 @@ msgid "Unable to log in with provided credentials."
msgstr ""
#: authtoken/serializers.py:26
-msgid "Must include \"username\" and \"password\""
+msgid "Must include \"username\" and \"password\"."
msgstr ""
#: exceptions.py:38
@@ -86,7 +86,7 @@ msgid "Not found."
msgstr ""
#: exceptions.py:98
-msgid "Method '{method}' not allowed."
+msgid "Method \"{method}\" not allowed."
msgstr ""
#: exceptions.py:109
@@ -94,7 +94,7 @@ msgid "Could not satisfy the request Accept header."
msgstr ""
#: exceptions.py:121
-msgid "Unsupported media type '{media_type}' in request."
+msgid "Unsupported media type \"{media_type}\" in request."
msgstr ""
#: exceptions.py:134
@@ -136,7 +136,8 @@ msgstr ""
#: fields.py:615
msgid ""
-"Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
msgstr ""
#: fields.py:627
@@ -235,15 +236,15 @@ msgstr ""
#: generics.py:123
msgid ""
"Choose a valid page number. Page numbers must be a whole number, or must be "
-"the string 'last'."
+"the string \"last\"."
msgstr ""
#: generics.py:128
-msgid "Invalid page ({page_number}): {message}."
+msgid "Invalid page \"{page_number}\": {message}."
msgstr ""
#: relations.py:132
-msgid "Invalid pk '{pk_value}' - object does not exist."
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
msgstr ""
#: relations.py:133
@@ -299,7 +300,7 @@ msgid "This field must be unique for the \"{date_field}\" year."
msgstr ""
#: versioning.py:39
-msgid "Invalid version in 'Accept' header."
+msgid "Invalid version in \"Accept\" header."
msgstr ""
#: versioning.py:70 versioning.py:112
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 42b624e7d..05ac3d1c6 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -128,9 +128,9 @@ class StringRelatedField(RelatedField):
class PrimaryKeyRelatedField(RelatedField):
default_error_messages = {
- 'required': _("This field is required."),
- 'does_not_exist': _("Invalid pk \"{pk_value}\" - object does not exist."),
- 'incorrect_type': _("Incorrect type. Expected pk value, received {data_type}."),
+ 'required': _('This field is required.'),
+ 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
}
def use_pk_only_optimization(self):
@@ -152,11 +152,11 @@ class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
default_error_messages = {
- 'required': _("This field is required."),
- 'no_match': _("Invalid hyperlink - No URL match."),
- 'incorrect_match': _("Invalid hyperlink - Incorrect URL match."),
- 'does_not_exist': _("Invalid hyperlink - Object does not exist."),
- 'incorrect_type': _("Incorrect type. Expected URL string, received {data_type}."),
+ 'required': _('This field is required.'),
+ 'no_match': _('Invalid hyperlink - No URL match.'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
+ 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
}
def __init__(self, view_name=None, **kwargs):
@@ -291,8 +291,8 @@ class SlugRelatedField(RelatedField):
"""
default_error_messages = {
- 'does_not_exist': _("Object with {slug_name}={value} does not exist."),
- 'invalid': _("Invalid value."),
+ 'does_not_exist': _('Object with {slug_name}={value} does not exist.'),
+ 'invalid': _('Invalid value.'),
}
def __init__(self, slug_field=None, **kwargs):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 9d7c8884a..623ed5865 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -296,7 +296,7 @@ def get_validation_error_detail(exc):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
default_error_messages = {
- 'invalid': _("Invalid data. Expected a dictionary, but got {datatype}.")
+ 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
}
@property
@@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer):
many = True
default_error_messages = {
- 'not_a_list': _("Expected a list of items but got type `{input_type}`.")
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
}
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index cf6f07180..e3719b8d5 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -19,7 +19,7 @@ class UniqueValidator:
Should be applied to an individual field on the serializer.
"""
- message = _("This field must be unique.")
+ message = _('This field must be unique.')
def __init__(self, queryset, message=None):
self.queryset = queryset
@@ -73,8 +73,8 @@ class UniqueTogetherValidator:
Should be applied to the serializer class, not to an individual field.
"""
- message = _("The fields {field_names} must make a unique set.")
- missing_message = _("This field is required.")
+ message = _('The fields {field_names} must make a unique set.')
+ missing_message = _('This field is required.')
def __init__(self, queryset, fields, message=None):
self.queryset = queryset
@@ -152,7 +152,7 @@ class UniqueTogetherValidator:
class BaseUniqueForValidator:
message = None
- missing_message = _("This field is required.")
+ missing_message = _('This field is required.')
def __init__(self, queryset, field, date_field, message=None):
self.queryset = queryset
@@ -216,7 +216,7 @@ class BaseUniqueForValidator:
class UniqueForDateValidator(BaseUniqueForValidator):
- message = _("This field must be unique for the \"{date_field}\" date.")
+ message = _('This field must be unique for the "{date_field}" date.')
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
@@ -231,7 +231,7 @@ class UniqueForDateValidator(BaseUniqueForValidator):
class UniqueForMonthValidator(BaseUniqueForValidator):
- message = _("This field must be unique for the \"{date_field}\" month.")
+ message = _('This field must be unique for the "{date_field}" month.')
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
@@ -244,7 +244,7 @@ class UniqueForMonthValidator(BaseUniqueForValidator):
class UniqueForYearValidator(BaseUniqueForValidator):
- message = _("This field must be unique for the \"{date_field}\" year.")
+ message = _('This field must be unique for the "{date_field}" year.')
def filter_queryset(self, attrs, queryset):
value = attrs[self.field]
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
index 819c32df8..e31c71e9b 100644
--- a/rest_framework/versioning.py
+++ b/rest_framework/versioning.py
@@ -36,7 +36,7 @@ class AcceptHeaderVersioning(BaseVersioning):
Host: example.com
Accept: application/json; version=1.0
"""
- invalid_version_message = _("Invalid version in \"Accept\" header.")
+ invalid_version_message = _('Invalid version in "Accept" header.')
def determine_version(self, request, *args, **kwargs):
media_type = _MediaType(request.accepted_media_type)
@@ -67,7 +67,7 @@ class URLPathVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _("Invalid version in URL path.")
+ invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
version = kwargs.get(self.version_param, self.default_version)
@@ -109,7 +109,7 @@ class NamespaceVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _("Invalid version in URL path.")
+ invalid_version_message = _('Invalid version in URL path.')
def determine_version(self, request, *args, **kwargs):
resolver_match = getattr(request, 'resolver_match', None)
@@ -135,7 +135,7 @@ class HostNameVersioning(BaseVersioning):
Accept: application/json
"""
hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
- invalid_version_message = _("Invalid version in hostname.")
+ invalid_version_message = _('Invalid version in hostname.')
def determine_version(self, request, *args, **kwargs):
hostname, seperator, port = request.get_host().partition(':')
@@ -157,7 +157,7 @@ class QueryParameterVersioning(BaseVersioning):
Host: example.com
Accept: application/json
"""
- invalid_version_message = _("Invalid version in query parameter.")
+ invalid_version_message = _('Invalid version in query parameter.')
def determine_version(self, request, *args, **kwargs):
version = request.query_params.get(self.version_param)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 61d39aff6..5ecb98573 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -439,7 +439,7 @@ class TestSlugField(FieldValues):
'slug-99': 'slug-99',
}
invalid_inputs = {
- 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
+ 'slug 99': ['Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.']
}
outputs = {}
field = serializers.SlugField()
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 94023c30a..fba8718f5 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -117,7 +117,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'PUT' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'})
def test_delete_root_view(self):
"""
@@ -127,7 +127,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'})
def test_post_cannot_set_id(self):
"""
@@ -181,7 +181,7 @@ class TestInstanceView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'})
def test_put_instance_view(self):
"""
diff --git a/tests/test_relations.py b/tests/test_relations.py
index 62353dc25..08c92242b 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -33,7 +33,7 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase):
with pytest.raises(serializers.ValidationError) as excinfo:
self.field.to_internal_value(4)
msg = excinfo.value.detail[0]
- assert msg == "Invalid pk '4' - object does not exist."
+ assert msg == 'Invalid pk "4" - object does not exist.'
def test_pk_related_lookup_invalid_type(self):
with pytest.raises(serializers.ValidationError) as excinfo:
From 58ec7669aed9ebd58fd6095c6a6437bf9f3cf7f1 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 18:22:30 +0000
Subject: [PATCH 050/121] swap backticks for double quotes
---
rest_framework/exceptions.py | 2 +-
rest_framework/fields.py | 12 ++++++------
rest_framework/locale/en_US/LC_MESSAGES/django.po | 8 ++++----
rest_framework/serializers.py | 2 +-
4 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index f62c9fe39..f954c13e5 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -52,7 +52,7 @@ class APIException(Exception):
# built in `ValidationError`. For example:
#
# from rest_framework import serializers
-# raise serializers.ValidationError("Value was invalid")
+# raise serializers.ValidationError('Value was invalid')
class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 76101608e..b80dea603 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -477,7 +477,7 @@ class Field(object):
class BooleanField(Field):
default_error_messages = {
- 'invalid': _('`{input}` is not a valid boolean.')
+ 'invalid': _('"{input}" is not a valid boolean.')
}
default_empty_html = False
initial = False
@@ -505,7 +505,7 @@ class BooleanField(Field):
class NullBooleanField(Field):
default_error_messages = {
- 'invalid': _('`{input}` is not a valid boolean.')
+ 'invalid': _('"{input}" is not a valid boolean.')
}
initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
@@ -972,7 +972,7 @@ class TimeField(Field):
class ChoiceField(Field):
default_error_messages = {
- 'invalid_choice': _('`{input}` is not a valid choice.')
+ 'invalid_choice': _('"{input}" is not a valid choice.')
}
def __init__(self, choices, **kwargs):
@@ -1016,8 +1016,8 @@ class ChoiceField(Field):
class MultipleChoiceField(ChoiceField):
default_error_messages = {
- 'invalid_choice': _('`{input}` is not a valid choice.'),
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'invalid_choice': _('"{input}" is not a valid choice.'),
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
default_empty_html = []
@@ -1118,7 +1118,7 @@ class ListField(Field):
child = None
initial = []
default_error_messages = {
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
index c8fc7f4d7..d98225ce9 100644
--- a/rest_framework/locale/en_US/LC_MESSAGES/django.po
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-07 12:28+0000\n"
+"POT-Creation-Date: 2015-01-07 18:21+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -111,7 +111,7 @@ msgid "This field may not be null."
msgstr ""
#: fields.py:480 fields.py:508
-msgid "`{input}` is not a valid boolean."
+msgid "\"{input}\" is not a valid boolean."
msgstr ""
#: fields.py:543
@@ -199,11 +199,11 @@ msgid "Time has wrong format. Use one of these formats instead: {format}."
msgstr ""
#: fields.py:975 fields.py:1019
-msgid "`{input}` is not a valid choice."
+msgid "\"{input}\" is not a valid choice."
msgstr ""
#: fields.py:1020 fields.py:1121 serializers.py:476
-msgid "Expected a list of items but got type `{input_type}`."
+msgid "Expected a list of items but got type \"{input_type}\"."
msgstr ""
#: fields.py:1050
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 623ed5865..5bfbd2351 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -473,7 +473,7 @@ class ListSerializer(BaseSerializer):
many = True
default_error_messages = {
- 'not_a_list': _('Expected a list of items but got type `{input_type}`.')
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
def __init__(self, *args, **kwargs):
From 734f8f26678d3bd28f04bc44b0fabd146b97ddb0 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 18:22:40 +0000
Subject: [PATCH 051/121] restore Django 404
---
rest_framework/generics.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7ebed0327..d52f2b6c0 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -120,13 +120,13 @@ class GenericAPIView(views.APIView):
if page == 'last':
page_number = paginator.num_pages
else:
- raise NotFound(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".'))
+ raise Http404(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".'))
try:
page = paginator.page(page_number)
except InvalidPage as exc:
error_format = _('Invalid page "{page_number}": {message}.')
- raise NotFound(error_format.format(
+ raise Http404(error_format.format(
page_number=page_number, message=six.text_type(exc)
))
From f0ad0a88c49f1fef473ef1fbf965bcaa974ee062 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 12:31:51 +0000
Subject: [PATCH 052/121] Link to Roy Fielding versioning interview.
---
docs/api-guide/versioning.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md
index 92380cc0e..7463f190b 100644
--- a/docs/api-guide/versioning.md
+++ b/docs/api-guide/versioning.md
@@ -10,6 +10,8 @@ API versioning allows you to alter behavior between different clients. REST fram
Versioning is determined by the incoming client request, and may either be based on the request URL, or based on the request headers.
+There are a number of valid approaches to approaching versioning. [Non-versioned systems can also be appropriate][roy-fielding-on-versioning], particularly if you're engineering for very long-term systems with multiple clients outside of your control.
+
## Versioning with REST framework
When API versioning is enabled, the `request.version` attribute will contain a string that corresponds to the version requested in the incoming client request.
@@ -195,6 +197,7 @@ The following example uses a custom `X-API-Version` header to determine the requ
If your versioning scheme is based on the request URL, you will also want to alter how versioned URLs are determined. In order to do so you should override the `.reverse()` method on the class. See the source code for examples.
[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
+[roy-fielding-on-versioning]: http://www.infoq.com/articles/roy-fielding-on-versioning
[klabnik-guidelines]: http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned
[heroku-guidelines]: https://github.com/interagent/http-api-design#version-with-accepts-header
[json-parameters]: http://tools.ietf.org/html/rfc4627#section-6
From 1368c31a705a4892995f42cf5e0dcdcbfa13a1ce Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Thu, 8 Jan 2015 17:16:15 +0000
Subject: [PATCH 053/121] remove unused import
---
rest_framework/generics.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index d52f2b6c0..0d709c37a 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -10,7 +10,6 @@ from django.shortcuts import get_object_or_404 as _get_object_or_404
from django.utils import six
from django.utils.translation import ugettext as _
from rest_framework import views, mixins
-from rest_framework.exceptions import NotFound
from rest_framework.settings import api_settings
From 7f8d314101c4e6e059b00ac12658f0e1055da8f7 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Thu, 8 Jan 2015 17:16:47 +0000
Subject: [PATCH 054/121] update tests to expect new error messages
---
tests/test_fields.py | 18 +++++++++---------
tests/test_serializer_bulk_update.py | 4 ++--
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 5ecb98573..240827eea 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -338,7 +338,7 @@ class TestBooleanField(FieldValues):
False: False,
}
invalid_inputs = {
- 'foo': ['`foo` is not a valid boolean.'],
+ 'foo': ['"foo" is not a valid boolean.'],
None: ['This field may not be null.']
}
outputs = {
@@ -368,7 +368,7 @@ class TestNullBooleanField(FieldValues):
None: None
}
invalid_inputs = {
- 'foo': ['`foo` is not a valid boolean.'],
+ 'foo': ['"foo" is not a valid boolean.'],
}
outputs = {
'true': True,
@@ -832,7 +832,7 @@ class TestChoiceField(FieldValues):
'good': 'good',
}
invalid_inputs = {
- 'amazing': ['`amazing` is not a valid choice.']
+ 'amazing': ['"amazing" is not a valid choice.']
}
outputs = {
'good': 'good',
@@ -872,8 +872,8 @@ class TestChoiceFieldWithType(FieldValues):
3: 3,
}
invalid_inputs = {
- 5: ['`5` is not a valid choice.'],
- 'abc': ['`abc` is not a valid choice.']
+ 5: ['"5" is not a valid choice.'],
+ 'abc': ['"abc" is not a valid choice.']
}
outputs = {
'1': 1,
@@ -899,7 +899,7 @@ class TestChoiceFieldWithListChoices(FieldValues):
'good': 'good',
}
invalid_inputs = {
- 'awful': ['`awful` is not a valid choice.']
+ 'awful': ['"awful" is not a valid choice.']
}
outputs = {
'good': 'good'
@@ -917,8 +917,8 @@ class TestMultipleChoiceField(FieldValues):
('aircon', 'manual'): set(['aircon', 'manual']),
}
invalid_inputs = {
- 'abc': ['Expected a list of items but got type `str`.'],
- ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
+ 'abc': ['Expected a list of items but got type "str".'],
+ ('aircon', 'incorrect'): ['"incorrect" is not a valid choice.']
}
outputs = [
(['aircon', 'manual'], set(['aircon', 'manual']))
@@ -1028,7 +1028,7 @@ class TestListField(FieldValues):
(['1', '2', '3'], [1, 2, 3])
]
invalid_inputs = [
- ('not a list', ['Expected a list of items but got type `str`.']),
+ ('not a list', ['Expected a list of items but got type "str".']),
([1, 2, 'error'], ['A valid integer is required.'])
]
outputs = [
diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py
index fb881a755..bc955b2ef 100644
--- a/tests/test_serializer_bulk_update.py
+++ b/tests/test_serializer_bulk_update.py
@@ -101,7 +101,7 @@ class BulkCreateSerializerTests(TestCase):
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
- expected_errors = {'non_field_errors': ['Expected a list of items but got type `int`.']}
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
self.assertEqual(serializer.errors, expected_errors)
@@ -118,6 +118,6 @@ class BulkCreateSerializerTests(TestCase):
serializer = self.BookSerializer(data=data, many=True)
self.assertEqual(serializer.is_valid(), False)
- expected_errors = {'non_field_errors': ['Expected a list of items but got type `dict`.']}
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
self.assertEqual(serializer.errors, expected_errors)
From 73feaf6299827607eab94ce96b77b73671880626 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 9 Jan 2015 15:30:36 +0000
Subject: [PATCH 055/121] First pass at 3.1 pagination API
---
rest_framework/generics.py | 220 +++++++++++----------------------
rest_framework/mixins.py | 13 +-
rest_framework/pagination.py | 231 +++++++++++++++++++++++++----------
rest_framework/settings.py | 4 +-
tests/test_pagination.py | 216 +-------------------------------
5 files changed, 248 insertions(+), 436 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 0d709c37a..12fb64138 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -2,29 +2,13 @@
Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
-
-from django.core.paginator import Paginator, InvalidPage
from django.db.models.query import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404
-from django.utils import six
-from django.utils.translation import ugettext as _
from rest_framework import views, mixins
from rest_framework.settings import api_settings
-def strict_positive_int(integer_string, cutoff=None):
- """
- Cast a string to a strictly positive integer.
- """
- ret = int(integer_string)
- if ret <= 0:
- raise ValueError()
- if cutoff:
- ret = min(ret, cutoff)
- return ret
-
-
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
"""
Same as Django's standard shortcut, but make sure to also raise 404
@@ -40,7 +24,6 @@ class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
-
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
# If you are overriding a view method, it is important that you call
@@ -50,146 +33,16 @@ class GenericAPIView(views.APIView):
queryset = None
serializer_class = None
- # If you want to use object lookups other than pk, set this attribute.
+ # If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
lookup_url_kwarg = None
- # Pagination settings
- paginate_by = api_settings.PAGINATE_BY
- paginate_by_param = api_settings.PAGINATE_BY_PARAM
- max_paginate_by = api_settings.MAX_PAGINATE_BY
- pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
- page_kwarg = 'page'
-
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
- # The following attribute may be subject to change,
- # and should be considered private API.
- paginator_class = Paginator
-
- def get_serializer_context(self):
- """
- Extra context provided to the serializer class.
- """
- return {
- 'request': self.request,
- 'format': self.format_kwarg,
- 'view': self
- }
-
- def get_serializer(self, *args, **kwargs):
- """
- Return the serializer instance that should be used for validating and
- deserializing input, and for serializing output.
- """
- serializer_class = self.get_serializer_class()
- kwargs['context'] = self.get_serializer_context()
- return serializer_class(*args, **kwargs)
-
- def get_pagination_serializer(self, page):
- """
- Return a serializer instance to use with paginated data.
- """
- class SerializerClass(self.pagination_serializer_class):
- class Meta:
- object_serializer_class = self.get_serializer_class()
-
- pagination_serializer_class = SerializerClass
- context = self.get_serializer_context()
- return pagination_serializer_class(instance=page, context=context)
-
- def paginate_queryset(self, queryset):
- """
- Paginate a queryset if required, either returning a page object,
- or `None` if pagination is not configured for this view.
- """
- page_size = self.get_paginate_by()
- if not page_size:
- return None
-
- paginator = self.paginator_class(queryset, page_size)
- page_kwarg = self.kwargs.get(self.page_kwarg)
- page_query_param = self.request.query_params.get(self.page_kwarg)
- page = page_kwarg or page_query_param or 1
- try:
- page_number = paginator.validate_number(page)
- except InvalidPage:
- if page == 'last':
- page_number = paginator.num_pages
- else:
- raise Http404(_('Choose a valid page number. Page numbers must be a whole number, or must be the string "last".'))
-
- try:
- page = paginator.page(page_number)
- except InvalidPage as exc:
- error_format = _('Invalid page "{page_number}": {message}.')
- raise Http404(error_format.format(
- page_number=page_number, message=six.text_type(exc)
- ))
-
- return page
-
- def filter_queryset(self, queryset):
- """
- Given a queryset, filter it with whichever filter backend is in use.
-
- You are unlikely to want to override this method, although you may need
- to call it either from a list view, or from a custom `get_object`
- method if you want to apply the configured filtering backend to the
- default queryset.
- """
- for backend in self.get_filter_backends():
- queryset = backend().filter_queryset(self.request, queryset, self)
- return queryset
-
- def get_filter_backends(self):
- """
- Returns the list of filter backends that this view requires.
- """
- return list(self.filter_backends)
-
- # The following methods provide default implementations
- # that you may want to override for more complex cases.
-
- def get_paginate_by(self):
- """
- Return the size of pages to use with pagination.
-
- If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
- from a named query parameter in the url, eg. ?page_size=100
-
- Otherwise defaults to using `self.paginate_by`.
- """
- if self.paginate_by_param:
- try:
- return strict_positive_int(
- self.request.query_params[self.paginate_by_param],
- cutoff=self.max_paginate_by
- )
- except (KeyError, ValueError):
- pass
-
- return self.paginate_by
-
- def get_serializer_class(self):
- """
- Return the class to use for the serializer.
- Defaults to using `self.serializer_class`.
-
- You may want to override this if you need to provide different
- serializations depending on the incoming request.
-
- (Eg. admins get full serialization, others get basic serialization)
- """
- assert self.serializer_class is not None, (
- "'%s' should either include a `serializer_class` attribute, "
- "or override the `get_serializer_class()` method."
- % self.__class__.__name__
- )
-
- return self.serializer_class
+ # The style to use for queryset pagination.
+ pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get_queryset(self):
"""
@@ -246,6 +99,73 @@ class GenericAPIView(views.APIView):
return obj
+ def get_serializer(self, *args, **kwargs):
+ """
+ Return the serializer instance that should be used for validating and
+ deserializing input, and for serializing output.
+ """
+ serializer_class = self.get_serializer_class()
+ kwargs['context'] = self.get_serializer_context()
+ return serializer_class(*args, **kwargs)
+
+ def get_serializer_class(self):
+ """
+ Return the class to use for the serializer.
+ Defaults to using `self.serializer_class`.
+
+ You may want to override this if you need to provide different
+ serializations depending on the incoming request.
+
+ (Eg. admins get full serialization, others get basic serialization)
+ """
+ assert self.serializer_class is not None, (
+ "'%s' should either include a `serializer_class` attribute, "
+ "or override the `get_serializer_class()` method."
+ % self.__class__.__name__
+ )
+
+ return self.serializer_class
+
+ def get_serializer_context(self):
+ """
+ Extra context provided to the serializer class.
+ """
+ return {
+ 'request': self.request,
+ 'format': self.format_kwarg,
+ 'view': self
+ }
+
+ def filter_queryset(self, queryset):
+ """
+ Given a queryset, filter it with whichever filter backend is in use.
+
+ You are unlikely to want to override this method, although you may need
+ to call it either from a list view, or from a custom `get_object`
+ method if you want to apply the configured filtering backend to the
+ default queryset.
+ """
+ for backend in list(self.filter_backends):
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ return queryset
+
+ @property
+ def pager(self):
+ if not hasattr(self, '_pager'):
+ if self.pagination_class is None:
+ self._pager = None
+ else:
+ self._pager = self.pagination_class()
+ return self._pager
+
+ def paginate_queryset(self, queryset):
+ if self.pager is None:
+ return None
+ return self.pager.paginate_queryset(queryset, self.request, view=self)
+
+ def get_paginated_response(self, objects):
+ return self.pager.get_paginated_response(objects)
+
# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 2074a1072..c34cfcee1 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -5,7 +5,6 @@ We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
-
from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings
@@ -37,12 +36,14 @@ class ListModelMixin(object):
List a queryset.
"""
def list(self, request, *args, **kwargs):
- instance = self.filter_queryset(self.get_queryset())
- page = self.paginate_queryset(instance)
+ queryset = self.filter_queryset(self.get_queryset())
+
+ page = self.paginate_queryset(queryset)
if page is not None:
- serializer = self.get_pagination_serializer(page)
- else:
- serializer = self.get_serializer(instance, many=True)
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+
+ serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index f31e5fa4c..da2d60a44 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -3,87 +3,192 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
-from rest_framework import serializers
+from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.utils import six
+from django.utils.translation import ugettext as _
+from rest_framework.compat import OrderedDict
+from rest_framework.exceptions import NotFound
+from rest_framework.response import Response
+from rest_framework.settings import api_settings
from rest_framework.templatetags.rest_framework import replace_query_param
-class NextPageField(serializers.Field):
+def _strict_positive_int(integer_string, cutoff=None):
"""
- Field that returns a link to the next page in paginated results.
+ Cast a string to a strictly positive integer.
"""
- page_field = 'page'
-
- def to_representation(self, value):
- if not value.has_next():
- return None
- page = value.next_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ ret = int(integer_string)
+ if ret <= 0:
+ raise ValueError()
+ if cutoff:
+ ret = min(ret, cutoff)
+ return ret
-class PreviousPageField(serializers.Field):
+class BasePagination(object):
+ def paginate_queryset(self, queryset, request):
+ raise NotImplemented('paginate_queryset() must be implemented.')
+
+ def get_paginated_response(self, data, page, request):
+ raise NotImplemented('get_paginated_response() must be implemented.')
+
+
+class PageNumberPagination(BasePagination):
"""
- Field that returns a link to the previous page in paginated results.
+ A simple page number based style that supports page numbers as
+ query parameters. For example:
+
+ http://api.example.org/accounts/?page=4
+ http://api.example.org/accounts/?page=4&page_size=100
"""
- page_field = 'page'
+ # The default page size.
+ # Defaults to `None`, meaning pagination is disabled.
+ paginate_by = api_settings.PAGINATE_BY
- def to_representation(self, value):
- if not value.has_previous():
- return None
- page = value.previous_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ # Client can control the page using this query parameter.
+ page_query_param = 'page'
+ # Client can control the page size using this query parameter.
+ # Default is 'None'. Set to eg 'page_size' to enable usage.
+ paginate_by_param = api_settings.PAGINATE_BY_PARAM
-class DefaultObjectSerializer(serializers.ReadOnlyField):
- """
- If no object serializer is specified, then this serializer will be applied
- as the default.
- """
+ # Set to an integer to limit the maximum page size the client may request.
+ # Only relevant if 'paginate_by_param' has also been set.
+ max_paginate_by = api_settings.MAX_PAGINATE_BY
- def __init__(self, source=None, many=None, context=None):
- # Note: Swallow context and many kwargs - only required for
- # eg. ModelSerializer.
- super(DefaultObjectSerializer, self).__init__(source=source)
-
-
-class BasePaginationSerializer(serializers.Serializer):
- """
- A base class for pagination serializers to inherit from,
- to make implementing custom serializers more easy.
- """
- results_field = 'results'
-
- def __init__(self, *args, **kwargs):
+ def paginate_queryset(self, queryset, request, view):
"""
- Override init to add in the object serializer field on-the-fly.
+ Paginate a queryset if required, either returning a page object,
+ or `None` if pagination is not configured for this view.
"""
- super(BasePaginationSerializer, self).__init__(*args, **kwargs)
- results_field = self.results_field
+ for attr in (
+ 'paginate_by', 'page_query_param',
+ 'paginate_by_param', 'max_paginate_by'
+ ):
+ if hasattr(view, attr):
+ setattr(self, attr, getattr(view, attr))
+
+ page_size = self.get_page_size(request)
+ if not page_size:
+ return None
+
+ paginator = DjangoPaginator(queryset, page_size)
+ page_string = request.query_params.get(self.page_query_param, 1)
+ try:
+ page_number = paginator.validate_number(page_string)
+ except InvalidPage:
+ if page_string == 'last':
+ page_number = paginator.num_pages
+ else:
+ msg = _(
+ 'Choose a valid page number. Page numbers must be a '
+ 'whole number, or must be the string "last".'
+ )
+ raise NotFound(msg)
try:
- object_serializer = self.Meta.object_serializer_class
- except AttributeError:
- object_serializer = DefaultObjectSerializer
+ self.page = paginator.page(page_number)
+ except InvalidPage as exc:
+ msg = _('Invalid page "{page_number}": {message}.').format(
+ page_number=page_number, message=six.text_type(exc)
+ )
+ raise NotFound(msg)
+ self.request = request
+ return self.page
+
+ def get_paginated_response(self, objects):
+ return Response(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', objects)
+ ]))
+
+ def get_page_size(self, request):
+ if self.paginate_by_param:
+ try:
+ return _strict_positive_int(
+ request.query_params[self.paginate_by_param],
+ cutoff=self.max_paginate_by
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.paginate_by
+
+ def get_next_link(self):
+ if not self.page.has_next():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.next_page_number()
+ return replace_query_param(url, self.page_query_param, page_number)
+
+ def get_previous_link(self):
+ if not self.page.has_previous():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.previous_page_number()
+ return replace_query_param(url, self.page_query_param, page_number)
+
+
+class LimitOffsetPagination(BasePagination):
+ """
+ A limit/offset based style. For example:
+
+ http://api.example.org/accounts/?limit=100
+ http://api.example.org/accounts/?offset=400&limit=100
+ """
+ default_limit = api_settings.PAGINATE_BY
+ limit_query_param = 'limit'
+ offset_query_param = 'offset'
+ max_limit = None
+
+ def paginate_queryset(self, queryset, request, view):
+ self.limit = self.get_limit(request)
+ self.offset = self.get_offset(request)
+ self.count = queryset.count()
+ self.request = request
+ return queryset[self.offset:self.offset + self.limit]
+
+ def get_paginated_response(self, objects):
+ return Response(OrderedDict([
+ ('count', self.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', objects)
+ ]))
+
+ def get_limit(self, request):
+ if self.limit_query_param:
+ try:
+ return _strict_positive_int(
+ request.query_params[self.limit_query_param],
+ cutoff=self.max_limit
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.default_limit
+
+ def get_offset(self, request):
try:
- list_serializer_class = object_serializer.Meta.list_serializer_class
- except AttributeError:
- list_serializer_class = serializers.ListSerializer
+ return _strict_positive_int(
+ request.query_params[self.offset_query_param],
+ )
+ except (KeyError, ValueError):
+ return 0
- self.fields[results_field] = list_serializer_class(
- child=object_serializer(),
- source='object_list'
- )
- self.fields[results_field].bind(field_name=results_field, parent=self)
+ def get_next_link(self, page):
+ if self.offset + self.limit >= self.count:
+ return None
+ url = self.request.build_absolute_uri()
+ offset = self.offset + self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
-
-class PaginationSerializer(BasePaginationSerializer):
- """
- A default implementation of a pagination serializer.
- """
- count = serializers.ReadOnlyField(source='paginator.count')
- next = NextPageField(source='*')
- previous = PreviousPageField(source='*')
+ def get_previous_link(self, page):
+ if self.offset - self.limit < 0:
+ return None
+ url = self.request.build_absolute_uri()
+ offset = self.offset - self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 877d461be..3cce26b1c 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -49,7 +49,7 @@ DEFAULTS = {
'DEFAULT_VERSIONING_CLASS': None,
# Generic view behavior
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_FILTER_BACKENDS': (),
# Throttling
@@ -130,7 +130,7 @@ IMPORT_STRINGS = (
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_METADATA_CLASS',
'DEFAULT_VERSIONING_CLASS',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
+ 'DEFAULT_PAGINATION_CLASS',
'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER',
'TEST_REQUEST_RENDERER_CLASSES',
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 1fd9cf9c4..d410cd5eb 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -1,10 +1,9 @@
from __future__ import unicode_literals
import datetime
from decimal import Decimal
-from django.core.paginator import Paginator
from django.test import TestCase
from django.utils import unittest
-from rest_framework import generics, serializers, status, pagination, filters
+from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory
from .models import BasicModel, FilterableItem
@@ -238,45 +237,6 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEqual(response.data['previous'], None)
-class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
- class Meta:
- object_serializer_class = serializers.Serializer
-
-
-class UnitTestPagination(TestCase):
- """
- Unit tests for pagination of primitive objects.
- """
-
- def setUp(self):
- self.objects = [char * 3 for char in 'abcdefghijklmnopqrstuvwxyz']
- paginator = Paginator(self.objects, 10)
- self.first_page = paginator.page(1)
- self.last_page = paginator.page(3)
-
- def test_native_pagination(self):
- serializer = pagination.PaginationSerializer(self.first_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], '?page=2')
- self.assertEqual(serializer.data['previous'], None)
- self.assertEqual(serializer.data['results'], self.objects[:10])
-
- serializer = pagination.PaginationSerializer(self.last_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], None)
- self.assertEqual(serializer.data['previous'], '?page=2')
- self.assertEqual(serializer.data['results'], self.objects[20:])
-
- def test_context_available_in_result(self):
- """
- Ensure context gets passed through to the object serializer.
- """
- serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'})
- serializer.data
- results = serializer.fields[serializer.results_field]
- self.assertEqual(serializer.context, results.context)
-
-
class TestUnpaginated(TestCase):
"""
Tests for list views without pagination.
@@ -377,177 +337,3 @@ class TestMaxPaginateByParam(TestCase):
request = factory.get('/')
response = self.view(request).render()
self.assertEqual(response.data['results'], self.data[:3])
-
-
-# Tests for context in pagination serializers
-
-class CustomField(serializers.ReadOnlyField):
- def to_native(self, value):
- if 'view' not in self.context:
- raise RuntimeError("context isn't getting passed into custom field")
- return "value"
-
-
-class BasicModelSerializer(serializers.Serializer):
- text = CustomField()
-
- def to_native(self, value):
- if 'view' not in self.context:
- raise RuntimeError("context isn't getting passed into serializer")
- return super(BasicSerializer, self).to_native(value)
-
-
-class TestContextPassedToCustomField(TestCase):
- def setUp(self):
- BasicModel.objects.create(text='ala ma kota')
-
- def test_with_pagination(self):
- class ListView(generics.ListCreateAPIView):
- queryset = BasicModel.objects.all()
- serializer_class = BasicModelSerializer
- paginate_by = 1
-
- self.view = ListView.as_view()
- request = factory.get('/')
- response = self.view(request).render()
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-# Tests for custom pagination serializers
-
-class LinksSerializer(serializers.Serializer):
- next = pagination.NextPageField(source='*')
- prev = pagination.PreviousPageField(source='*')
-
-
-class CustomPaginationSerializer(pagination.BasePaginationSerializer):
- links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.ReadOnlyField(source='paginator.count')
-
- results_field = 'objects'
-
-
-class CustomFooSerializer(serializers.Serializer):
- foo = serializers.CharField()
-
-
-class CustomFooPaginationSerializer(pagination.PaginationSerializer):
- class Meta:
- object_serializer_class = CustomFooSerializer
-
-
-class TestCustomPaginationSerializer(TestCase):
- def setUp(self):
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = Paginator(objects, 2)
- self.page = paginator.page(1)
-
- def test_custom_pagination_serializer(self):
- request = APIRequestFactory().get('/foobar')
- serializer = CustomPaginationSerializer(
- instance=self.page,
- context={'request': request}
- )
- expected = {
- 'links': {
- 'next': 'http://testserver/foobar?page=2',
- 'prev': None
- },
- 'total_results': 4,
- 'objects': ['john', 'paul']
- }
- self.assertEqual(serializer.data, expected)
-
- def test_custom_pagination_serializer_with_custom_object_serializer(self):
- objects = [
- {'foo': 'bar'},
- {'foo': 'spam'}
- ]
- paginator = Paginator(objects, 1)
- page = paginator.page(1)
- serializer = CustomFooPaginationSerializer(page)
- serializer.data
-
-
-class NonIntegerPage(object):
-
- def __init__(self, paginator, object_list, prev_token, token, next_token):
- self.paginator = paginator
- self.object_list = object_list
- self.prev_token = prev_token
- self.token = token
- self.next_token = next_token
-
- def has_next(self):
- return not not self.next_token
-
- def next_page_number(self):
- return self.next_token
-
- def has_previous(self):
- return not not self.prev_token
-
- def previous_page_number(self):
- return self.prev_token
-
-
-class NonIntegerPaginator(object):
-
- def __init__(self, object_list, per_page):
- self.object_list = object_list
- self.per_page = per_page
-
- def count(self):
- # pretend like we don't know how many pages we have
- return None
-
- def page(self, token=None):
- if token:
- try:
- first = self.object_list.index(token)
- except ValueError:
- first = 0
- else:
- first = 0
- n = len(self.object_list)
- last = min(first + self.per_page, n)
- prev_token = self.object_list[last - (2 * self.per_page)] if first else None
- next_token = self.object_list[last] if last < n else None
- return NonIntegerPage(self, self.object_list[first:last], prev_token, token, next_token)
-
-
-class TestNonIntegerPagination(TestCase):
- def test_custom_pagination_serializer(self):
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = NonIntegerPaginator(objects, 2)
-
- request = APIRequestFactory().get('/foobar')
- serializer = CustomPaginationSerializer(
- instance=paginator.page(),
- context={'request': request}
- )
- expected = {
- 'links': {
- 'next': 'http://testserver/foobar?page={0}'.format(objects[2]),
- 'prev': None
- },
- 'total_results': None,
- 'objects': objects[:2]
- }
- self.assertEqual(serializer.data, expected)
-
- request = APIRequestFactory().get('/foobar')
- serializer = CustomPaginationSerializer(
- instance=paginator.page('george'),
- context={'request': request}
- )
- expected = {
- 'links': {
- 'next': None,
- 'prev': 'http://testserver/foobar?page={0}'.format(objects[0]),
- },
- 'total_results': None,
- 'objects': objects[2:]
- }
- self.assertEqual(serializer.data, expected)
From 2b28026fc10c2a8d3e4c9ef1f11b2f802a40ec77 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 13 Jan 2015 10:44:40 +0000
Subject: [PATCH 056/121] Translation info -> project management
---
CONTRIBUTING.md | 125 +++++++++++-------------------
docs/topics/project-management.md | 55 ++++++++++++-
2 files changed, 99 insertions(+), 81 deletions(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d94eb87e0..c9626ebff 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,9 +10,9 @@ There are many ways you can contribute to Django REST framework. We'd like it t
The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
-If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular Javascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
+If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
-Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
+Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
@@ -52,7 +52,7 @@ To start developing on Django REST framework, clone the repo:
git clone git@github.com:tomchristie/django-rest-framework.git
-Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles.
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
## Testing
@@ -60,13 +60,47 @@ To run the tests, clone the repository, and then:
# Setup the virtual environment
virtualenv env
- env/bin/activate
+ source env/bin/activate
pip install -r requirements.txt
# Run the tests
./runtests.py
-You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
+### Test options
+
+Run using a more concise output style.
+
+ ./runtests.py -q
+
+Run the tests using a more concise output style, no coverage, no flake8.
+
+ ./runtests.py --fast
+
+Don't run the flake8 code linting.
+
+ ./runtests.py --nolint
+
+Only run the flake8 code linting, don't run the tests.
+
+ ./runtests.py --lintonly
+
+Run the tests for a given test case.
+
+ ./runtests.py MyTestCase
+
+Run the tests for a given test method.
+
+ ./runtests.py MyTestCase.test_this_method
+
+Shorter form to run the tests for a given test method.
+
+ ./runtests.py test_this_method
+
+Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
+
+### Running against multiple environments
+
+You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
tox
@@ -82,7 +116,7 @@ GitHub's documentation for working on pull requests is [available here][pull-req
Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
-Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are running as you'd expect.
+Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
![Travis status][travis-status]
@@ -96,7 +130,7 @@ Sometimes, in order to ensure your code works on various different versions of D
The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
-There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
+There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
## Building the documentation
@@ -104,7 +138,7 @@ To build the documentation, install MkDocs with `pip install mkdocs` and then ru
mkdocs build
-This will build the html output into the `html` directory.
+This will build the documentation into the `site` directory.
You can build the documentation and open a preview in a browser window by using the `serve` command.
@@ -117,8 +151,7 @@ Documentation should be in American English. The tone of the documentation is v
Some other tips:
* Keep paragraphs reasonably short.
-* Use double spacing after the end of sentences.
-* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'.
+* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
## Markdown style
@@ -151,7 +184,7 @@ If you are hyperlinking to another REST framework document, you should use a rel
[authentication]: ../api-guide/authentication.md
-Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
+Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
##### 3. Notes
@@ -163,70 +196,6 @@ If you want to draw attention to a note or warning, use a pair of enclosing line
---
-# Third party packages
-
-New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI.
-
-## Getting started
-
-If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging.
-
-We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package.
-
-## Linking to your package
-
-Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
-
-# Translations
-
-If REST framework isn't translated into your language you can request that it is at the [Transifex project][transifex].
-
-## Managing Transfiex
-The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
-
-```
-pip install transifex-client
-```
-
-To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your authentication information:
-
-```
-[https://www.transifex.com]
-username = user
-token =
-password = p@ssw0rd
-hostname = https://www.transifex.com
-```
-
-## Upload new source translations
-When any user-visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
-
-```
-cd rest_framework
-django-admin.py makemessages -l en_US
-cd ..
-tx push -s
-```
-
-When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
-
-Here's how differences between the old and new source files will be handled:
-
-* New strings will be added.
-* Modified strings will be added as well.
-* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically restore the translated string too.
-
-
-## Get translations
-When a translator has finished translating their work needs to be downloaded from Transifex into the source repo. To do this, run:
-
-```
-tx pull -a
-cd rest_framework
-django-admin.py compilemessages
-```
-
-You can then commit as normal.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/
@@ -234,13 +203,9 @@ You can then commit as normal.
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
[pep-8]: http://www.python.org/dev/peps/pep-0008/
-[travis-status]: https://raw.github.com/tomchristie/django-rest-framework/master/docs/img/travis-status.png
+[travis-status]: ../img/travis-status.png
[pull-requests]: https://help.github.com/articles/using-pull-requests
[tox]: http://tox.readthedocs.org/en/latest/
[markdown]: http://daringfireball.net/projects/markdown/basics
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
-[django-reusable-app]: https://github.com/dabapps/django-reusable-app
-[transifex]: https://www.transifex.com/projects/p/django-rest-framework/
-[transifex-client]: https://pypi.python.org/pypi/transifex-client
-[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
\ No newline at end of file
diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md
index f581cabd3..7f7051966 100644
--- a/docs/topics/project-management.md
+++ b/docs/topics/project-management.md
@@ -63,10 +63,11 @@ The following template should be used for the description of the issue, and serv
Team members have the following responsibilities.
-* Add triage labels and milestones to tickets.
* Close invalid or resolved tickets.
+* Add triage labels and milestones to tickets.
* Merge finalized pull requests.
* Build and deploy the documentation, using `mkdocs gh-deploy`.
+* Build and update the included translation packs.
Further notes for maintainers:
@@ -112,6 +113,55 @@ When pushing the release to PyPI ensure that your environment has been installed
---
+## Translations
+
+The maintenance team are responsible for managing the translation packs include in REST framework. Translating the source strings into multiple languages is managed through the [transifex service][transifex-project].
+
+### Managing Transifex
+
+The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
+
+ pip install transifex-client
+
+To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your credentials.
+
+ [https://www.transifex.com]
+ username = ***
+ token = ***
+ password = ***
+ hostname = https://www.transifex.com
+
+### Upload new source files
+
+When any user visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
+
+ # 1. Update the source django.po file, which is the US English version.
+ cd rest_framework
+ django-admin.py makemessages -l en_US
+ # 2. Push the source django.po file to Transifex.
+ cd ..
+ tx push -s
+
+When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
+
+Here's how differences between the old and new source files will be handled:
+
+* New strings will be added.
+* Modified strings will be added as well.
+* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically include the translation string.
+
+### Download translations
+
+When a translator has finished translating their work needs to be downloaded from Transifex into the REST framework repository. To do this, run:
+
+ # 3. Pull the translated django.po files from Transifex.
+ tx pull -a
+ cd rest_framework
+ # 4. Compile the binary .mo files for all supported languages.
+ django-admin.py compilemessages
+
+---
+
## Project ownership
The PyPI package is owned by `@tomchristie`. As a backup `@j4mie` also has ownership of the package.
@@ -129,6 +179,9 @@ The following issues still need to be addressed:
[bus-factor]: http://en.wikipedia.org/wiki/Bus_factor
[un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
+[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
+[transifex-client]: https://pypi.python.org/pypi/transifex-client
+[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
[github-org]: https://github.com/tomchristie/django-rest-framework/issues/2162
[sandbox]: http://restframework.herokuapp.com/
[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
From 8e2dc6b26dd546f6b31aa6d1feb881b181f3ea21 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 13 Jan 2015 12:07:25 +0000
Subject: [PATCH 057/121] Update internationalization docs
---
docs/index.md | 1 +
docs/topics/internationalisation.md | 95 -----------------------------
docs/topics/internationalization.md | 72 ++++++++++++++++++++++
mkdocs.yml | 1 +
4 files changed, 74 insertions(+), 95 deletions(-)
delete mode 100644 docs/topics/internationalisation.md
create mode 100644 docs/topics/internationalization.md
diff --git a/docs/index.md b/docs/index.md
index 544204c65..163769851 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -305,6 +305,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[settings]: api-guide/settings.md
[documenting-your-api]: topics/documenting-your-api.md
+[internationalization]: topics/documenting-your-api.md
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
[browser-enhancements]: topics/browser-enhancements.md
[browsableapi]: topics/browsable-api.md
diff --git a/docs/topics/internationalisation.md b/docs/topics/internationalisation.md
deleted file mode 100644
index 2a476c864..000000000
--- a/docs/topics/internationalisation.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Internationalisation
-REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation] and by translating the messages into your language.
-
-## How to translate REST Framework errors
-
-REST framework translations are managed online using [Transifex.com][transifex]. To get started, checkout the guide in the [CONTRIBUTING.md guide][contributing].
-
-Sometimes you may want to use REST Framework in a language which has not been translated yet on Transifex. If that is the case then you should translate the error messages locally.
-
-#### How to translate REST Framework error messages locally:
-
-This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
-
-1. Make a new folder where you want to store the translated errors. Add this
-path to your [`LOCALE_PATHS`][django-locale-paths] setting.
-
- ---
-
- **Note:** For the rest of
-this document we will assume the path you created was
-`/home/www/project/conf/locale/`, and that you have updated your `settings.py` to include the setting:
-
- ```
- LOCALE_PATHS = (
- '/home/www/project/conf/locale/',
- )
- ```
-
- ---
-
-2. Now create a subfolder for the language you want to translate. The folder should be named using [locale
-name][django-locale-name] notation. E.g. `de`, `pt_BR`, `es_AR`, etc.
-
- ```
- mkdir /home/www/project/conf/locale/pt_BR/LC_MESSAGES
- ```
-
-3. Now copy the base translations file from the REST framework source code
-into your translations folder
-
- ```
- cp /home/user/.virtualenvs/myproject/lib/python2.7/site-packages/rest_framework/locale/en_US/LC_MESSAGES/django.po
- /home/www/project/conf/locale/pt_BR/LC_MESSAGES
- ```
-
- This should create the file
- `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po`
-
- ---
-
- **Note:** To find out where `rest_framework` is installed, run
-
- ```
- python -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())"
- ```
-
- ---
-
-
-4. Edit `/home/www/project/conf/locale/pt_BR/LC_MESSAGES/django.po` and
-translate all the error messages.
-
-5. Run `manage.py compilemessages -l pt_BR` to make the translations
-available for Django to use. You should see a message
-
- ```
- processing file django.po in /home/www/project/conf/locale/pt_BR/LC_MESSAGES
- ```
-
-6. Restart your server.
-
-
-
-## How Django chooses which language to use
-REST framework will use the same preferences to select which language to
-display as Django does. You can find more info in the [Django docs on discovering language preferences][django-language-preference]. For reference, these are
-
-1. First, it looks for the language prefix in the requested URL
-2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
-3. Failing that, it looks for a cookie
-4. Failing that, it looks at the `Accept-Language` HTTP header.
-5. Failing that, it uses the global `LANGUAGE_CODE` setting.
-
----
-
-**Note:** You'll need to include the `django.middleware.locale.LocaleMiddleware` to enable any of the per-request language preferences.
-
----
-
-
-[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
-[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
-[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
-[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
-[contributing]: ../../CONTRIBUTING.md
diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md
new file mode 100644
index 000000000..fdde6c43a
--- /dev/null
+++ b/docs/topics/internationalization.md
@@ -0,0 +1,72 @@
+# Internationalization
+
+> Supporting internationalization is not optional. It must be a core feature.
+>
+> — [Jannis Leidel, speaking at Django Under the Hood, 2015][cite].
+
+REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation].
+
+Doing so will allow you to:
+
+* Select a language other than English as the default, using the standard `LANGUAGE_CODE` Django setting.
+* Allow clients to choose a language themselves, using the `LocaleMiddleware` included with Django. A typical usage for API clients would be to include an `Accept-Language` request header.
+
+Note that the translations only apply to the error strings themselves. The format of error messages, and the keys of field names will remain the same. An example `400 Bad Request` response body might look like this:
+
+ {"detail": {"username": ["Esse campo deve ser unico."]}}
+
+If you want to use different string for parts of the response such as `detail` and `non_field_errors` then you can modify this behavior by using a [custom exception handler][custom-exception-handler].
+
+## Adding new translations
+
+REST framework translations are managed online using [Transifex][transifex-project]. You can use the Transifex service to add new translation languages. The maintenance team will then ensure that these translation strings are included in the REST framework package.
+
+Sometimes you may need to add translation strings to your project locally. You may need to do this if:
+
+* You want to use REST Framework in a language which has not been translated yet on Transifex.
+* Your project includes custom error messages, which are not part of REST framework's default translation strings.
+
+#### Translating a new language locally
+
+This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
+
+If you're translating a new language you'll need to translate the existing REST framework error messages:
+
+1. Make a new folder where you want to store the internationalization resources. Add this path to your [`LOCALE_PATHS`][django-locale-paths] setting.
+
+2. Now create a subfolder for the language you want to translate. The folder should be named using [locale name][django-locale-name] notation. For example: `de`, `pt_BR`, `es_AR`.
+
+3. Now copy the [base translations file][django-po-source] from the REST framework source code into your translations folder.
+
+4. Edit the `django.po` file you've just copied, translating all the error messages.
+
+5. Run `manage.py compilemessages -l pt_BR` to make the translations
+available for Django to use. You should see a message like `processing file django.po in <...>/locale/pt_BR/LC_MESSAGES`.
+
+6. Restart your development server to see the changes take effect.
+
+If you're only translating custom error messages that exist inside your project codebase you don't need to copy the REST framework source `django.po` file into a `LOCALE_PATHS` folder, and can instead simply run Django's standard `makemessages` process.
+
+## How the language is determined
+
+If you want to allow per-request language preferences you'll need to include `django.middleware.locale.LocaleMiddleware` in your `MIDDLEWARE_CLASSES` setting.
+
+You can find more information on how the language preference is determined in the [Django documentation][django-language-preference]. For reference, the method is:
+
+1. First, it looks for the language prefix in the requested URL.
+2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
+3. Failing that, it looks for a cookie.
+4. Failing that, it looks at the `Accept-Language` HTTP header.
+5. Failing that, it uses the global `LANGUAGE_CODE` setting.
+
+For API clients the most appropriate of these will typically be to use the `Accept-Language` header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an `Accept-Language` header for API clients rather than using language URL prefixes.
+
+[cite]: http://youtu.be/Wa0VfS2q94Y
+[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
+[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
+[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
+[django-po-source]: https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
+[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
+[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
+[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
+[contributing]: ../../CONTRIBUTING.md
diff --git a/mkdocs.yml b/mkdocs.yml
index b394a827d..89df4cea5 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -42,6 +42,7 @@ pages:
- ['api-guide/testing.md', 'API Guide', 'Testing']
- ['api-guide/settings.md', 'API Guide', 'Settings']
- ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
+ - ['topics/internationalization.md', 'Topics', 'Internationalization']
- ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
- ['topics/browser-enhancements.md', 'Topics',]
- ['topics/browsable-api.md', 'Topics', 'The Browsable API']
From 564f845e21cd55669311db9491b85dc86a5ff628 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 13 Jan 2015 12:21:03 +0000
Subject: [PATCH 058/121] Lower header font weights for nicer docs style
---
docs/topics/3.1-announcement.md | 7 +++++++
docs_theme/css/default.css | 15 +++++++++++++++
2 files changed, 22 insertions(+)
create mode 100644 docs/topics/3.1-announcement.md
diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md
new file mode 100644
index 000000000..a0ad98299
--- /dev/null
+++ b/docs/topics/3.1-announcement.md
@@ -0,0 +1,7 @@
+# Versioning
+
+# Pagination
+
+# Internationalization
+
+# ModelSerializer API
diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css
index 48d00366b..3feff0bad 100644
--- a/docs_theme/css/default.css
+++ b/docs_theme/css/default.css
@@ -171,6 +171,21 @@ body{
background-attachment: fixed;
}
+
+#main-content h1:first-of-type {
+ margin-top: 0
+}
+
+#main-content h1, #main-content h2 {
+ font-weight: 300;
+ margin-top: 20px
+}
+
+#main-content h3, #main-content h4, #main-content h5 {
+ font-weight: 500;
+ margin-top: 15px
+}
+
/* custom navigation styles */
.navbar .navbar-inner{
From 1bcec3a0ac4346b31b655a08505d3e3dc2156604 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 13 Jan 2015 17:14:13 +0000
Subject: [PATCH 059/121] API tweaks and pagination documentation
---
docs/api-guide/pagination.md | 174 +++++++++++++----------------------
rest_framework/generics.py | 6 +-
rest_framework/pagination.py | 28 ++++--
3 files changed, 86 insertions(+), 122 deletions(-)
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 834292920..9fbeb22a0 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -6,148 +6,101 @@ source: pagination.py
>
> — [Django documentation][cite]
-REST framework includes a `PaginationSerializer` class that makes it easy to return paginated data in a way that can then be rendered to arbitrary media types.
+REST framework includes support for customizable pagination styles. This allows you to modify how large result sets are split into individual pages of data.
-## Paginating basic data
+The pagination API can support either:
-Let's start by taking a look at an example from the Django documentation.
+* Pagination links that are provided as part of the content of the response.
+* Pagination links that are included in response headers, such as `Content-Range` or `Link`.
- from django.core.paginator import Paginator
+The built-in styles currently all use links included as part of the content of the response. This style is more accessible when using the browsable API.
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = Paginator(objects, 2)
- page = paginator.page(1)
- page.object_list
- # ['john', 'paul']
+Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular `APIView`, you'll need to call into the pagination API yourself to ensure you return a paginated response. See the source code for the `mixins.ListMixin` and `generics.GenericAPIView` classes for an example.
-At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results.
+## Setting the pagination style
- from rest_framework.pagination import PaginationSerializer
-
- serializer = PaginationSerializer(instance=page)
- serializer.data
- # {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']}
-
-The `context` argument of the `PaginationSerializer` class may optionally include the request. If the request is included in the context then the next and previous links returned by the serializer will use absolute URLs instead of relative URLs.
-
- request = RequestFactory().get('/foobar')
- serializer = PaginationSerializer(instance=page, context={'request': request})
- serializer.data
- # {'count': 4, 'next': 'http://testserver/foobar?page=2', 'previous': None, 'results': [u'john', u'paul']}
-
-We could now return that data in a `Response` object, and it would be rendered into the correct media type.
-
-## Paginating QuerySets
-
-Our first example worked because we were using primitive objects. If we wanted to paginate a queryset or other complex data, we'd need to specify a serializer to use to serialize the result set itself.
-
-We can do this using the `object_serializer_class` attribute on the inner `Meta` class of the pagination serializer. For example.
-
- class UserSerializer(serializers.ModelSerializer):
- """
- Serializes user querysets.
- """
- class Meta:
- model = User
- fields = ('username', 'email')
-
- class PaginatedUserSerializer(pagination.PaginationSerializer):
- """
- Serializes page objects of user querysets.
- """
- class Meta:
- object_serializer_class = UserSerializer
-
-We could now use our pagination serializer in a view like this.
-
- @api_view('GET')
- def user_list(request):
- queryset = User.objects.all()
- paginator = Paginator(queryset, 20)
-
- page = request.QUERY_PARAMS.get('page')
- try:
- users = paginator.page(page)
- except PageNotAnInteger:
- # If page is not an integer, deliver first page.
- users = paginator.page(1)
- except EmptyPage:
- # If page is out of range (e.g. 9999),
- # deliver last page of results.
- users = paginator.page(paginator.num_pages)
-
- serializer_context = {'request': request}
- serializer = PaginatedUserSerializer(users,
- context=serializer_context)
- return Response(serializer.data)
-
-## Pagination in the generic views
-
-The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
-
-The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY`, `PAGINATE_BY_PARAM`, and `MAX_PAGINATE_BY` settings. For example.
+The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example, to use the built-in limit/offset pagination, you would do:
REST_FRAMEWORK = {
- 'PAGINATE_BY': 10, # Default to 10
- 'PAGINATE_BY_PARAM': 'page_size', # Allow client to override, using `?page_size=xxx`.
- 'MAX_PAGINATE_BY': 100 # Maximum limit allowed when using `?page_size=xxx`.
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}
-You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
+You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis.
- class PaginatedListView(ListAPIView):
- queryset = ExampleModel.objects.all()
- serializer_class = ExampleModelSerializer
- paginate_by = 10
+## Modifying the pagination style
+
+If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change.
+
+ class LargeResultsSetPagination(PageNumberPagination):
+ paginate_by = 1000
paginate_by_param = 'page_size'
- max_paginate_by = 100
+ max_paginate_by = 10000
-Note that using a `paginate_by` value of `None` will turn off pagination for the view.
-Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter.
+ class StandardResultsSetPagination(PageNumberPagination):
+ paginate_by = 100
+ paginate_by_param = 'page_size'
+ max_paginate_by = 1000
-For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
+You can then apply your new style to a view using the `.pagination_class` attribute:
+
+ class BillingRecordsView(generics.ListAPIView):
+ queryset = Billing.objects.all()
+ serializer = BillingRecordsSerializer
+ pagination_class = LargeResultsSetPagination
+
+Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination'
}
+
+# API Reference
+
+## PageNumberPagination
+
+## LimitOffsetPagination
---
-# Custom pagination serializers
+# Custom pagination styles
-To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return.
+To create a custom pagination serializer class you should subclass `pagination.BasePagination` and override the `paginate_queryset(self, queryset, request, view)` and `get_paginated_response(self, data)` methods:
-You can also override the name used for the object list field, by setting the `results_field` attribute, which defaults to `'results'`.
+* The `paginate_queryset` method is passed the initial queryset and should return an iterable object that contains only the data in the requested page.
+* The `get_paginated_response` method is passed the serialized page data and should return a `Response` instance.
+
+Note that the `paginate_queryset` method may set state on the pagination instance, that may later be used by the `get_paginated_response` method.
## Example
-For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
+Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
- from rest_framework import pagination
- from rest_framework import serializers
+ class LinkHeaderPagination(PageNumberPagination)
+ def get_paginated_response(self, data):
+ next_url = self.get_next_link()
previous_url = self.get_previous_link()
- class LinksSerializer(serializers.Serializer):
- next = pagination.NextPageField(source='*')
- prev = pagination.PreviousPageField(source='*')
+ if next_url is not None and previous_url is not None:
+ link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
+ elif next_url is not None:
+ link = '<{next_url}; rel="next">'
+ elif prev_url is not None:
+ link = '<{previous_url}; rel="prev">'
+ else:
+ link = ''
- class CustomPaginationSerializer(pagination.BasePaginationSerializer):
- links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.ReadOnlyField(source='paginator.count')
+ link = link.format(next_url=next_url, previous_url=previous_url)
+ headers = {'Link': link} if link else {}
- results_field = 'objects'
+ return Response(data, headers=headers)
-## Using your custom pagination serializer
+## Using your custom pagination class
-To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
+To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
REST_FRAMEWORK = {
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
- 'example_app.pagination.CustomPaginationSerializer',
+ 'DEFAULT_PAGINATION_CLASS':
+ 'my_project.apps.core.pagination.LinkHeaderPagination',
}
-Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
-
- class PaginatedListView(generics.ListAPIView):
- model = ExampleModel
- pagination_serializer_class = CustomPaginationSerializer
- paginate_by = 10
-
# Third party packages
The following third party packages are also available.
@@ -157,5 +110,6 @@ The following third party packages are also available.
The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
+[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 12fb64138..cdf6ece08 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -160,11 +160,11 @@ class GenericAPIView(views.APIView):
def paginate_queryset(self, queryset):
if self.pager is None:
- return None
+ return queryset
return self.pager.paginate_queryset(queryset, self.request, view=self)
- def get_paginated_response(self, objects):
- return self.pager.get_paginated_response(objects)
+ def get_paginated_response(self, data):
+ return self.pager.get_paginated_response(data)
# Concrete view classes that provide method handlers
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index da2d60a44..b9d487968 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -25,11 +25,21 @@ def _strict_positive_int(integer_string, cutoff=None):
return ret
+def _get_count(queryset):
+ """
+ Determine an object count, supporting either querysets or regular lists.
+ """
+ try:
+ return queryset.count()
+ except AttributeError:
+ return len(queryset)
+
+
class BasePagination(object):
- def paginate_queryset(self, queryset, request):
+ def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.')
- def get_paginated_response(self, data, page, request):
+ def get_paginated_response(self, data):
raise NotImplemented('get_paginated_response() must be implemented.')
@@ -58,8 +68,8 @@ class PageNumberPagination(BasePagination):
def paginate_queryset(self, queryset, request, view):
"""
- Paginate a queryset if required, either returning a page object,
- or `None` if pagination is not configured for this view.
+ Paginate a queryset if required, either returning a
+ page object, or `None` if pagination is not configured for this view.
"""
for attr in (
'paginate_by', 'page_query_param',
@@ -97,12 +107,12 @@ class PageNumberPagination(BasePagination):
self.request = request
return self.page
- def get_paginated_response(self, objects):
+ def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.page.paginator.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
- ('results', objects)
+ ('results', data)
]))
def get_page_size(self, request):
@@ -147,16 +157,16 @@ class LimitOffsetPagination(BasePagination):
def paginate_queryset(self, queryset, request, view):
self.limit = self.get_limit(request)
self.offset = self.get_offset(request)
- self.count = queryset.count()
+ self.count = _get_count(queryset)
self.request = request
return queryset[self.offset:self.offset + self.limit]
- def get_paginated_response(self, objects):
+ def get_paginated_response(self, data):
return Response(OrderedDict([
('count', self.count),
('next', self.get_next_link()),
('previous', self.get_previous_link()),
- ('results', objects)
+ ('results', data)
]))
def get_limit(self, request):
From 4d287c7aef7b12086930eeb7a05cadb7e8b2cc48 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 13:19:56 +0000
Subject: [PATCH 060/121] Include paragraph around view description in browable
API
---
rest_framework/utils/formatting.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 470af51b0..173848df7 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -59,4 +59,5 @@ def markup_description(description):
description = apply_markdown(description)
else:
description = escape(description).replace('\n', ' ')
+ description = '
' + description + '
'
return mark_safe(description)
From f13fcba9a9f41f7e00e0ea8956fcc65ca168c76c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 13:20:02 +0000
Subject: [PATCH 061/121] Include paragraph around view description in browable
API
---
rest_framework/utils/formatting.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 173848df7..8b6f005e1 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -2,12 +2,10 @@
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
-import re
-
from django.utils.html import escape
from django.utils.safestring import mark_safe
-
from rest_framework.compat import apply_markdown, force_text
+import re
def remove_trailing_string(content, trailing):
From 3833a5bb8a9174e5fb09dac59a964eff24b6065e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 16:51:26 +0000
Subject: [PATCH 062/121] Include pagination control in browsable API
---
rest_framework/pagination.py | 90 ++++++++++++++++++-
rest_framework/renderers.py | 1 +
.../rest_framework/css/bootstrap-tweaks.css | 4 -
.../templates/rest_framework/base.html | 9 ++
.../rest_framework/pagination/numbers.html | 27 ++++++
rest_framework/templatetags/rest_framework.py | 17 ++++
6 files changed, 143 insertions(+), 5 deletions(-)
create mode 100644 rest_framework/templates/rest_framework/pagination/numbers.html
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index b9d487968..bd343c0dd 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -3,14 +3,18 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
+from collections import namedtuple
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.template import Context, loader
from django.utils import six
from django.utils.translation import ugettext as _
from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.templatetags.rest_framework import replace_query_param
+from rest_framework.templatetags.rest_framework import (
+ replace_query_param, remove_query_param
+)
def _strict_positive_int(integer_string, cutoff=None):
@@ -35,6 +39,49 @@ def _get_count(queryset):
return len(queryset)
+def _get_displayed_page_numbers(current, final):
+ """
+ This utility function determines a list of page numbers to display.
+ This gives us a nice contextually relevant set of page numbers.
+
+ For example:
+ current=14, final=16 -> [1, None, 13, 14, 15, 16]
+ """
+ assert current >= 1
+ assert final >= current
+
+ # We always include the first two pages, last two pages, and
+ # two pages either side of the current page.
+ included = set((
+ 1,
+ current - 1, current, current + 1,
+ final
+ ))
+
+ # If the break would only exclude a single page number then we
+ # may as well include the page number instead of the break.
+ if current == 4:
+ included.add(2)
+ if current == final - 3:
+ included.add(final - 1)
+
+ # Now sort the page numbers and drop anything outside the limits.
+ included = [
+ idx for idx in sorted(list(included))
+ if idx > 0 and idx <= final
+ ]
+
+ # Finally insert any `...` breaks
+ if current > 4:
+ included.insert(1, None)
+ if current < final - 3:
+ included.insert(len(included) - 1, None)
+ return included
+
+
+PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
+
+
class BasePagination(object):
def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.')
@@ -66,6 +113,8 @@ class PageNumberPagination(BasePagination):
# Only relevant if 'paginate_by_param' has also been set.
max_paginate_by = api_settings.MAX_PAGINATE_BY
+ template = 'rest_framework/pagination/numbers.html'
+
def paginate_queryset(self, queryset, request, view):
"""
Paginate a queryset if required, either returning a
@@ -104,6 +153,8 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
+ # Indicate that the browsable API should display pagination controls.
+ self.mark_as_used = True
self.request = request
return self.page
@@ -139,8 +190,45 @@ class PageNumberPagination(BasePagination):
return None
url = self.request.build_absolute_uri()
page_number = self.page.previous_page_number()
+ if page_number == 1:
+ return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number)
+ def to_html(self):
+ current = self.page.number
+ final = self.page.paginator.num_pages
+
+ page_links = []
+ base_url = self.request.build_absolute_uri()
+ for page_number in _get_displayed_page_numbers(current, final):
+ if page_number is None:
+ page_link = PageLink(
+ url=None,
+ number=None,
+ is_active=False,
+ is_break=True
+ )
+ else:
+ if page_number == 1:
+ url = remove_query_param(base_url, self.page_query_param)
+ else:
+ url = replace_query_param(url, self.page_query_param, page_number)
+ page_link = PageLink(
+ url=url,
+ number=page_number,
+ is_active=(page_number == current),
+ is_break=False
+ )
+ page_links.append(page_link)
+
+ template = loader.get_template(self.template)
+ context = Context({
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ })
+ return template.render(context)
+
class LimitOffsetPagination(BasePagination):
"""
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index c4de30db7..4c002b168 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -592,6 +592,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'description': self.get_description(view),
'name': self.get_name(view),
'version': VERSION,
+ 'pager': getattr(view, 'pager', None),
'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 36c7be481..d4a7d31a2 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -185,10 +185,6 @@ body a:hover {
color: #c20000;
}
-#content a span {
- text-decoration: underline;
- }
-
.request-info {
clear:both;
}
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e96681932..e00309811 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -119,9 +119,18 @@
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 69e03af40..bf159d8b1 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -26,6 +26,23 @@ def replace_query_param(url, key, val):
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+def remove_query_param(url, key):
+ """
+ Given a URL and a key/val pair, set or replace an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = QueryDict(query).copy()
+ query_dict.pop(key, None)
+ query = query_dict.urlencode()
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+
+@register.simple_tag
+def get_pagination_html(pager):
+ return pager.to_html()
+
+
# Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
From 313aa727e3c44016e531a7af75051fc6e6d7cb96 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 17:46:41 +0000
Subject: [PATCH 063/121] Tweaks
---
docs/api-guide/pagination.md | 19 +++++++++++++++----
docs/img/link-header-pagination.png | Bin 0 -> 35799 bytes
rest_framework/pagination.py | 12 ++++++++++--
3 files changed, 25 insertions(+), 6 deletions(-)
create mode 100644 docs/img/link-header-pagination.png
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 9fbeb22a0..ba71a3032 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -74,7 +74,7 @@ Note that the `paginate_queryset` method may set state on the pagination instanc
Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
- class LinkHeaderPagination(PageNumberPagination)
+ class LinkHeaderPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
@@ -82,7 +82,7 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu
link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
elif next_url is not None:
link = '<{next_url}; rel="next">'
- elif prev_url is not None:
+ elif previous_url is not None:
link = '<{previous_url}; rel="prev">'
else:
link = ''
@@ -97,10 +97,20 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
REST_FRAMEWORK = {
- 'DEFAULT_PAGINATION_CLASS':
- 'my_project.apps.core.pagination.LinkHeaderPagination',
+ 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination',
+ 'PAGINATE_BY': 10
}
+API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example:
+
+---
+
+![Link Header][link-header]
+
+*A custom pagination style, using the 'Link' header'*
+
+---
+
# Third party packages
The following third party packages are also available.
@@ -111,5 +121,6 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
+[link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
diff --git a/docs/img/link-header-pagination.png b/docs/img/link-header-pagination.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3c556a4d6aebc3dc1575c43eced4059d864bab2
GIT binary patch
literal 35799
zcmZTv19W9c*NvU-*y-4|)v?*JZQEwYb|>lBwr$(#*f#!j&wMkP^=Ga3)_rxWPF0;-
zr}p0GRzhT?gkhmDpa1{>U`0g)$whv9&TZ
zF*g7J5P>aCa8f{2!R$MeaLob5$0fRsum=(;2PB)~Q}~JiLDk1=?5J-8s)&RPFQaIG
z;9D$b5e&MVy--0jfd8W;43x6q7=(K(?sd0yyv1%g?FxXmOdk@2tO5q0
z@Y!Ccq8^iro;)Su37Z!H7=jmFM){c0$4q8|{=D4akvm&x2piF+%q#xo3{d40OE@e5
zl0Qtp&D266M5cf#wHmGjI6yD&)>85T5M4hmC5-@aa0TxOa;&tGeHV5
zz_~t(coG0WA#lhv!vdtpmnmTX(fc$3ZUBAGE-CXbVSW{#yZQ(Uc#to
ze@P_cu5N6=U8QpNL7Yy`f*wJ9@TaOEWgBtuiJiX}y?$=+?g(Jz7{y+EdPrkynFt2m
zwdUePRc;_w&XyjAi0-j-J_UqMAxtR0Z`s@JL%_er+0AnnZ=(?T95WJ|iGZsj-ZCMc
zy_x5sWyMaT;O=S1HXV?KAnBfsYKJ$5_$adF8`sFem5&Y)qNRtOce}(
z(t)6ihy2s7y5|nKp&VWx8WGX?z+P#4*Dm}(f_r%;$x_GA61L|pS{b;b8Qks-T%g-4
z(jXJOV+YAr7`d$6_n4V^G_4*tj_Qg|RJ<6l%MXwlZy$x4f+pg17Xc{^
zs{0xs1F7%{+xIX_D@$sh(;oRS=*w6B3D#?rN0=OKddknmT}|f*Nr25=p4Ox-2vXkj
zU2N80T7aqjiZ_WHV7WhQ1)X*Jol`hNfyxjf!RGqV^U=wi7Vs)Cl7Ar*un<80AtpOp
zV5opvgz5m!6r95A@l|h9ff3un*OB+>NB4yJwd%EyJ$w_EMvx`HmcTmS=PZ)Rby}qF
zP<;@3kvh8gbdX6B65M%6@_r`0>Rl_{-?j59l__D1gKB#pwR0*&7aSJwj{(i0>oAui
zvHPSptyziE;K$%mgY`D2Y#th38i-feRxDN^P6;ynmHO2-TF)n)xw%obLajP7d)NCS
zw~Qd@L9l(f@S-~K^btcLg&{QjQ~lw)U}O^tgzX6f5n_YYf(?2Mw}`e7w>S_&eu`ER
zP>~SDFvkvw^ouBo@c)$7XRE?nOvmC!iFS%I8(iHE+IB*M5=`9s8j|-@-cja|43%`8
z6eqVpE>BK^EMkVZ5UP;5a9OTA7jlYys%)zGr`=ESTxpT@xQr;gf&HO|;R=1`A%y|^
zA(#P)K{eSr$kX78SY{Ln+)9Ys#d{