From baa518cd890103173dd18857c609432bd47c6be4 Mon Sep 17 00:00:00 2001 From: Jharrod LaFon Date: Fri, 5 Sep 2014 15:30:01 -0700 Subject: [PATCH 001/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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

- 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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/301] 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 @@ +
{% block description %} {{ description }} {% endblock %} +
+ + {% if pager.mark_as_used %} + + {% endif %} +
{{ request.method }} {{ request.get_full_path }}
diff --git a/rest_framework/templates/rest_framework/pagination/numbers.html b/rest_framework/templates/rest_framework/pagination/numbers.html new file mode 100644 index 000000000..040458104 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/numbers.html @@ -0,0 +1,27 @@ +
    + {% if previous_url %} +
  • + {% else %} +
  • + {% endif %} + + {% for page_link in page_links %} + {% if page_link.is_break %} +
  • + +
  • + {% else %} + {% if page_link.is_active %} +
  • {{ page_link.number }}
  • + {% else %} +
  • {{ page_link.number }}
  • + {% endif %} + {% endif %} + {% endfor %} + + {% if next_url %} +
  • + {% else %} +
  • + {% endif %} +
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/301] 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 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

FdM1>*;W0Ix=Z$X}_;J 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 Hu0mjfzM=e` literal 0 HcmV?d00001 diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index bd343c0dd..69d0f77d3 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -46,6 +46,12 @@ def _get_displayed_page_numbers(current, final): For example: current=14, final=16 -> [1, None, 13, 14, 15, 16] + + This implementation gives one page to each side of the cursor, + for an implementation which gives two pages to each side of the cursor, + which is a copy of how GitHub treat pagination in their issue lists, see: + + https://gist.github.com/tomchristie/321140cebb1c4a558b15 """ assert current >= 1 assert final >= current @@ -60,10 +66,12 @@ def _get_displayed_page_numbers(current, 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: + if current <= 4: included.add(2) - if current == final - 3: + included.add(3) + if current >= final - 3: included.add(final - 1) + included.add(final - 2) # Now sort the page numbers and drop anything outside the limits. included = [ From d76e83dd78627a0cf4bcd4b28a7710fb678d8d4e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 15 Jan 2015 16:52:07 +0000 Subject: [PATCH 064/301] Tweaks, and add pagination controls for offset/limit. --- rest_framework/generics.py | 16 +-- rest_framework/pagination.py | 126 +++++++++++++----- rest_framework/renderers.py | 7 +- .../rest_framework/css/bootstrap-tweaks.css | 7 + .../templates/rest_framework/base.html | 4 +- 5 files changed, 119 insertions(+), 41 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index cdf6ece08..4cc4c64d2 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -150,21 +150,21 @@ class GenericAPIView(views.APIView): return queryset @property - def pager(self): - if not hasattr(self, '_pager'): + def paginator(self): + if not hasattr(self, '_paginator'): if self.pagination_class is None: - self._pager = None + self._paginator = None else: - self._pager = self.pagination_class() - return self._pager + self._paginator = self.pagination_class() + return self._paginator def paginate_queryset(self, queryset): - if self.pager is None: + if self.paginator is None: return queryset - return self.pager.paginate_queryset(queryset, self.request, view=self) + return self.paginator.paginate_queryset(queryset, self.request, view=self) def get_paginated_response(self, data): - return self.pager.get_paginated_response(data) + return self.paginator.get_paginated_response(data) # Concrete view classes that provide method handlers diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 69d0f77d3..2b78f1f7f 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -29,6 +29,15 @@ def _strict_positive_int(integer_string, cutoff=None): return ret +def _divide_with_ceil(a, b): + """ + Returns 'a' divded by 'b', with any remainder rounded up. + """ + if a % b: + return (a / b) + 1 + return a / b + + def _get_count(queryset): """ Determine an object count, supporting either querysets or regular lists. @@ -48,14 +57,21 @@ def _get_displayed_page_numbers(current, final): current=14, final=16 -> [1, None, 13, 14, 15, 16] This implementation gives one page to each side of the cursor, - for an implementation which gives two pages to each side of the cursor, - which is a copy of how GitHub treat pagination in their issue lists, see: + or two pages to the side when the cursor is at the edge, then + ensures that any breaks between non-continous page numbers never + remove only a single page. + + For an alernativative implementation which gives two pages to each side of + the cursor, eg. as in GitHub issue list pagination, see: https://gist.github.com/tomchristie/321140cebb1c4a558b15 """ assert current >= 1 assert final >= current + if final <= 5: + return range(1, final + 1) + # We always include the first two pages, last two pages, and # two pages either side of the current page. included = set(( @@ -87,16 +103,46 @@ def _get_displayed_page_numbers(current, final): return included +def _get_page_links(page_numbers, current, url_func): + """ + Given a list of page numbers and `None` page breaks, + return a list of `PageLink` objects. + """ + page_links = [] + for page_number in page_numbers: + if page_number is None: + page_link = PageLink( + url=None, + number=None, + is_active=False, + is_break=True + ) + else: + page_link = PageLink( + url=url_func(page_number), + number=page_number, + is_active=(page_number == current), + is_break=False + ) + page_links.append(page_link) + return page_links + + PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break']) class BasePagination(object): + display_page_controls = False + def paginate_queryset(self, queryset, request, view): raise NotImplemented('paginate_queryset() must be implemented.') def get_paginated_response(self, data): raise NotImplemented('get_paginated_response() must be implemented.') + def to_html(self): + raise NotImplemented('to_html() must be implemented to display page controls.') + class PageNumberPagination(BasePagination): """ @@ -161,8 +207,9 @@ class PageNumberPagination(BasePagination): ) raise NotFound(msg) - # Indicate that the browsable API should display pagination controls. - self.mark_as_used = True + if paginator.count > 1: + # The browsable API should display pagination controls. + self.display_page_controls = True self.request = request return self.page @@ -203,31 +250,17 @@ class PageNumberPagination(BasePagination): return replace_query_param(url, self.page_query_param, page_number) def to_html(self): + base_url = self.request.build_absolute_uri() + def page_number_to_url(page_number): + if page_number == 1: + return remove_query_param(base_url, self.page_query_param) + else: + return replace_query_param(base_url, self.page_query_param, page_number) + 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) + page_numbers = _get_displayed_page_numbers(current, final) + page_links = _get_page_links(page_numbers, current, page_number_to_url) template = loader.get_template(self.template) context = Context({ @@ -250,11 +283,15 @@ class LimitOffsetPagination(BasePagination): offset_query_param = 'offset' max_limit = None + template = 'rest_framework/pagination/numbers.html' + def paginate_queryset(self, queryset, request, view): self.limit = self.get_limit(request) self.offset = self.get_offset(request) self.count = _get_count(queryset) self.request = request + if self.count > self.limit: + self.display_page_controls = True return queryset[self.offset:self.offset + self.limit] def get_paginated_response(self, data): @@ -285,16 +322,45 @@ class LimitOffsetPagination(BasePagination): except (KeyError, ValueError): return 0 - def get_next_link(self, page): + def get_next_link(self): 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) - def get_previous_link(self, page): - if self.offset - self.limit < 0: + def get_previous_link(self): + if self.offset <= 0: return None + url = self.request.build_absolute_uri() + + if self.offset - self.limit <= 0: + return remove_query_param(url, self.offset_query_param) + offset = self.offset - self.limit return replace_query_param(url, self.offset_query_param, offset) + + def to_html(self): + base_url = self.request.build_absolute_uri() + current = _divide_with_ceil(self.offset, self.limit) + 1 + final = _divide_with_ceil(self.count, self.limit) + + def page_number_to_url(page_number): + if page_number == 1: + return remove_query_param(base_url, self.offset_query_param) + else: + offset = self.offset + ((page_number - current) * self.limit) + return replace_query_param(base_url, self.offset_query_param, offset) + + page_numbers = _get_displayed_page_numbers(current, final) + page_links = _get_page_links(page_numbers, current, page_number_to_url) + + 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) \ No newline at end of file diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c002b168..4c46b049f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -584,6 +584,11 @@ class BrowsableAPIRenderer(BaseRenderer): renderer_content_type += ' ;%s' % renderer.charset response_headers['Content-Type'] = renderer_content_type + if hasattr(view, 'paginator') and view.paginator.display_page_controls: + paginator = view.paginator + else: + paginator = None + context = { 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, @@ -592,7 +597,7 @@ class BrowsableAPIRenderer(BaseRenderer): 'description': self.get_description(view), 'name': self.get_name(view), 'version': VERSION, - 'pager': getattr(view, 'pager', None), + 'paginator': paginator, '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 d4a7d31a2..15b42178f 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -60,6 +60,13 @@ a single block in the template. color: #C20000; } +.pagination>.disabled>a, +.pagination>.disabled>a:hover, +.pagination>.disabled>a:focus { + cursor: default; + pointer-events: none; +} + /*=== dabapps bootstrap styles ====*/ html { diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index e00309811..877387f28 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,9 +125,9 @@ {% endblock %} - {% if pager.mark_as_used %} + {% if paginator %}

{% endif %} From 68dfa369b5ca877643b41c8df7c5fc0c786a9f08 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 15 Jan 2015 16:55:04 +0000 Subject: [PATCH 065/301] Flake 8 fixes --- rest_framework/pagination.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 2b78f1f7f..61b8e07ac 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -251,6 +251,7 @@ class PageNumberPagination(BasePagination): def to_html(self): base_url = self.request.build_absolute_uri() + def page_number_to_url(page_number): if page_number == 1: return remove_query_param(base_url, self.page_query_param) @@ -363,4 +364,4 @@ class LimitOffsetPagination(BasePagination): 'next_url': self.get_next_link(), 'page_links': page_links }) - return template.render(context) \ No newline at end of file + return template.render(context) From 53edd37df5aa0ac29dbe7824db2e33da1d901f98 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 15 Jan 2015 21:07:05 +0000 Subject: [PATCH 066/301] Tests for LimitOffsetPagination --- docs/api-guide/pagination.md | 2 +- rest_framework/pagination.py | 57 ++++++++--------- tests/test_pagination.py | 117 ++++++++++++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 32 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index ba71a3032..8ab2edd53 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -63,7 +63,7 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. # Custom pagination styles -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: +To create a custom pagination serializer class you should subclass `pagination.BasePagination` and override the `paginate_queryset(self, queryset, request, view=None)` and `get_paginated_response(self, data)` methods: * 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. diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 61b8e07ac..0dac56830 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -44,7 +44,7 @@ def _get_count(queryset): """ try: return queryset.count() - except AttributeError: + except (AttributeError, TypeError): return len(queryset) @@ -111,12 +111,7 @@ def _get_page_links(page_numbers, current, url_func): page_links = [] for page_number in page_numbers: if page_number is None: - page_link = PageLink( - url=None, - number=None, - is_active=False, - is_break=True - ) + page_link = PAGE_BREAK else: page_link = PageLink( url=url_func(page_number), @@ -130,11 +125,13 @@ def _get_page_links(page_numbers, current, url_func): PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break']) +PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True) + class BasePagination(object): display_page_controls = False - def paginate_queryset(self, queryset, request, view): + def paginate_queryset(self, queryset, request, view=None): raise NotImplemented('paginate_queryset() must be implemented.') def get_paginated_response(self, data): @@ -167,9 +164,11 @@ class PageNumberPagination(BasePagination): # Only relevant if 'paginate_by_param' has also been set. max_paginate_by = api_settings.MAX_PAGINATE_BY + last_page_strings = ('last',) + template = 'rest_framework/pagination/numbers.html' - def paginate_queryset(self, queryset, request, view): + def paginate_queryset(self, queryset, request, view=None): """ Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view. @@ -186,18 +185,9 @@ class PageNumberPagination(BasePagination): 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) + page_number = request.query_params.get(self.page_query_param, 1) + if page_number in self.last_page_strings: + page_number = paginator.num_pages try: self.page = paginator.page(page_number) @@ -210,6 +200,7 @@ class PageNumberPagination(BasePagination): if paginator.count > 1: # The browsable API should display pagination controls. self.display_page_controls = True + self.request = request return self.page @@ -249,7 +240,7 @@ class PageNumberPagination(BasePagination): return remove_query_param(url, self.page_query_param) return replace_query_param(url, self.page_query_param, page_number) - def to_html(self): + def get_html_context(self): base_url = self.request.build_absolute_uri() def page_number_to_url(page_number): @@ -263,12 +254,15 @@ class PageNumberPagination(BasePagination): page_numbers = _get_displayed_page_numbers(current, final) page_links = _get_page_links(page_numbers, current, page_number_to_url) - template = loader.get_template(self.template) - context = Context({ + return { 'previous_url': self.get_previous_link(), 'next_url': self.get_next_link(), 'page_links': page_links - }) + } + + def to_html(self): + template = loader.get_template(self.template) + context = Context(self.get_html_context()) return template.render(context) @@ -286,7 +280,7 @@ class LimitOffsetPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' - def paginate_queryset(self, queryset, request, view): + def paginate_queryset(self, queryset, request, view=None): self.limit = self.get_limit(request) self.offset = self.get_offset(request) self.count = _get_count(queryset) @@ -343,7 +337,7 @@ class LimitOffsetPagination(BasePagination): offset = self.offset - self.limit return replace_query_param(url, self.offset_query_param, offset) - def to_html(self): + def get_html_context(self): base_url = self.request.build_absolute_uri() current = _divide_with_ceil(self.offset, self.limit) + 1 final = _divide_with_ceil(self.count, self.limit) @@ -358,10 +352,13 @@ class LimitOffsetPagination(BasePagination): page_numbers = _get_displayed_page_numbers(current, final) page_links = _get_page_links(page_numbers, current, page_number_to_url) - template = loader.get_template(self.template) - context = Context({ + return { 'previous_url': self.get_previous_link(), 'next_url': self.get_next_link(), 'page_links': page_links - }) + } + + def to_html(self): + template = loader.get_template(self.template) + context = Context(self.get_html_context()) return template.render(context) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index d410cd5eb..32fe7a66f 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -3,8 +3,10 @@ import datetime from decimal import Decimal from django.test import TestCase from django.utils import unittest -from rest_framework import generics, serializers, status, filters +from rest_framework import generics, pagination, serializers, status, filters from rest_framework.compat import django_filters +from rest_framework.request import Request +from rest_framework.pagination import PageLink, PAGE_BREAK from rest_framework.test import APIRequestFactory from .models import BasicModel, FilterableItem @@ -337,3 +339,116 @@ class TestMaxPaginateByParam(TestCase): request = factory.get('/') response = self.view(request).render() self.assertEqual(response.data['results'], self.data[:3]) + + +class TestLimitOffset: + def setup(self): + self.pagination = pagination.LimitOffsetPagination() + self.queryset = range(1, 101) + + def paginate_queryset(self, request): + return self.pagination.paginate_queryset(self.queryset, request) + + def get_paginated_content(self, queryset): + response = self.pagination.get_paginated_response(queryset) + return response.data + + def get_html_context(self): + return self.pagination.get_html_context() + + def test_no_offset(self): + request = Request(factory.get('/', {'limit': 5})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [1, 2, 3, 4, 5] + assert content == { + 'results': [1, 2, 3, 4, 5], + 'previous': None, + 'next': 'http://testserver/?limit=5&offset=5', + 'count': 100 + } + assert context == { + 'previous_url': None, + 'next_url': 'http://testserver/?limit=5&offset=5', + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, True, False), + PageLink('http://testserver/?limit=5&offset=5', 2, False, False), + PageLink('http://testserver/?limit=5&offset=10', 3, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=95', 20, False, False), + ] + } + + def test_first_offset(self): + request = Request(factory.get('/', {'limit': 5, 'offset': 5})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [6, 7, 8, 9, 10] + assert content == { + 'results': [6, 7, 8, 9, 10], + 'previous': 'http://testserver/?limit=5', + 'next': 'http://testserver/?limit=5&offset=10', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?limit=5', + 'next_url': 'http://testserver/?limit=5&offset=10', + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, False, False), + PageLink('http://testserver/?limit=5&offset=5', 2, True, False), + PageLink('http://testserver/?limit=5&offset=10', 3, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=95', 20, False, False), + ] + } + + def test_middle_offset(self): + request = Request(factory.get('/', {'limit': 5, 'offset': 10})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [11, 12, 13, 14, 15] + assert content == { + 'results': [11, 12, 13, 14, 15], + 'previous': 'http://testserver/?limit=5&offset=5', + 'next': 'http://testserver/?limit=5&offset=15', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?limit=5&offset=5', + 'next_url': 'http://testserver/?limit=5&offset=15', + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, False, False), + PageLink('http://testserver/?limit=5&offset=5', 2, False, False), + PageLink('http://testserver/?limit=5&offset=10', 3, True, False), + PageLink('http://testserver/?limit=5&offset=15', 4, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=95', 20, False, False), + ] + } + + def test_ending_offset(self): + request = Request(factory.get('/', {'limit': 5, 'offset': 95})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [96, 97, 98, 99, 100] + assert content == { + 'results': [96, 97, 98, 99, 100], + 'previous': 'http://testserver/?limit=5&offset=90', + 'next': None, + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?limit=5&offset=90', + 'next_url': None, + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=85', 18, False, False), + PageLink('http://testserver/?limit=5&offset=90', 19, False, False), + PageLink('http://testserver/?limit=5&offset=95', 20, True, False), + ] + } From 50db8c092ab51a5eb94e2bb495c317097fceeb59 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Jan 2015 16:55:28 +0000 Subject: [PATCH 067/301] Minor test cleanup --- tests/test_metadata.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 5ff59c723..972a896a4 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,9 +1,7 @@ from __future__ import unicode_literals - -from rest_framework import exceptions, serializers, views +from rest_framework import exceptions, serializers, status, views from rest_framework.request import Request from rest_framework.test import APIRequestFactory -import pytest request = Request(APIRequestFactory().options('/')) @@ -17,7 +15,8 @@ class TestMetadata: """Example view.""" pass - response = ExampleView().options(request=request) + view = ExampleView.as_view() + response = view(request=request) expected = { 'name': 'Example', 'description': 'Example view.', @@ -31,7 +30,7 @@ class TestMetadata: 'multipart/form-data' ] } - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK assert response.data == expected def test_none_metadata(self): @@ -42,8 +41,10 @@ class TestMetadata: class ExampleView(views.APIView): metadata_class = None - with pytest.raises(exceptions.MethodNotAllowed): - ExampleView().options(request=request) + view = ExampleView.as_view() + response = view(request=request) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {'detail': 'Method "OPTIONS" not allowed.'} def test_actions(self): """ @@ -63,7 +64,8 @@ class TestMetadata: def get_serializer(self): return ExampleSerializer() - response = ExampleView().options(request=request) + view = ExampleView.as_view() + response = view(request=request) expected = { 'name': 'Example', 'description': 'Example view.', @@ -104,7 +106,7 @@ class TestMetadata: } } } - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK assert response.data == expected def test_global_permissions(self): @@ -132,8 +134,9 @@ class TestMetadata: if request.method == 'POST': raise exceptions.PermissionDenied() - response = ExampleView().options(request=request) - assert response.status_code == 200 + view = ExampleView.as_view() + response = view(request=request) + assert response.status_code == status.HTTP_200_OK assert list(response.data['actions'].keys()) == ['PUT'] def test_object_permissions(self): @@ -161,6 +164,7 @@ class TestMetadata: if self.request.method == 'PUT': raise exceptions.PermissionDenied() - response = ExampleView().options(request=request) - assert response.status_code == 200 + view = ExampleView.as_view() + response = view(request=request) + assert response.status_code == status.HTTP_200_OK assert list(response.data['actions'].keys()) == ['POST'] From 8b0f25aa0a91cb7b56f9ce4dde4330fe5daaad9b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Jan 2015 16:55:46 +0000 Subject: [PATCH 068/301] More pagination tests & cleanup --- rest_framework/generics.py | 12 +- rest_framework/pagination.py | 31 +- tests/test_pagination.py | 643 ++++++++++++++++++----------------- 3 files changed, 366 insertions(+), 320 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4cc4c64d2..61dcb84a4 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -151,6 +151,9 @@ class GenericAPIView(views.APIView): @property def paginator(self): + """ + The paginator instance associated with the view, or `None`. + """ if not hasattr(self, '_paginator'): if self.pagination_class is None: self._paginator = None @@ -159,11 +162,18 @@ class GenericAPIView(views.APIView): return self._paginator def paginate_queryset(self, queryset): + """ + Return a single page of results, or `None` if pagination is disabled. + """ if self.paginator is None: - return queryset + return None return self.paginator.paginate_queryset(queryset, self.request, view=self) def get_paginated_response(self, data): + """ + Return a paginated style `Response` object for the given output data. + """ + assert self.paginator is not None return self.paginator.get_paginated_response(data) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 0dac56830..c5a364f0a 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -131,13 +131,13 @@ PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True) class BasePagination(object): display_page_controls = False - def paginate_queryset(self, queryset, request, view=None): + def paginate_queryset(self, queryset, request, view=None): # pragma: no cover raise NotImplemented('paginate_queryset() must be implemented.') - def get_paginated_response(self, data): + def get_paginated_response(self, data): # pragma: no cover raise NotImplemented('get_paginated_response() must be implemented.') - def to_html(self): + def to_html(self): # pragma: no cover raise NotImplemented('to_html() must be implemented to display page controls.') @@ -168,10 +168,11 @@ class PageNumberPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' - def paginate_queryset(self, queryset, request, view=None): + def _handle_backwards_compat(self, view): """ - Paginate a queryset if required, either returning a - page object, or `None` if pagination is not configured for this view. + Prior to version 3.1, pagination was handled in the view, and the + attributes were set there. The attributes should now be set on + the pagination class, but the old style is still pending deprecation. """ for attr in ( 'paginate_by', 'page_query_param', @@ -180,6 +181,13 @@ class PageNumberPagination(BasePagination): if hasattr(view, attr): setattr(self, attr, getattr(view, attr)) + def paginate_queryset(self, queryset, request, view=None): + """ + Paginate a queryset if required, either returning a + page object, or `None` if pagination is not configured for this view. + """ + self._handle_backwards_compat(view) + page_size = self.get_page_size(request) if not page_size: return None @@ -277,7 +285,6 @@ class LimitOffsetPagination(BasePagination): limit_query_param = 'limit' offset_query_param = 'offset' max_limit = None - template = 'rest_framework/pagination/numbers.html' def paginate_queryset(self, queryset, request, view=None): @@ -340,7 +347,15 @@ class LimitOffsetPagination(BasePagination): def get_html_context(self): base_url = self.request.build_absolute_uri() current = _divide_with_ceil(self.offset, self.limit) + 1 - final = _divide_with_ceil(self.count, self.limit) + # The number of pages is a little bit fiddly. + # We need to sum both the number of pages from current offset to end + # plus the number of pages up to the current offset. + # When offset is not strictly divisible by the limit then we may + # end up introducing an extra page as an artifact. + final = ( + _divide_with_ceil(self.count - self.offset, self.limit) + + _divide_with_ceil(self.offset, self.limit) + ) def page_number_to_url(page_number): if page_number == 1: diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 32fe7a66f..b3436b359 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,349 +1,270 @@ from __future__ import unicode_literals -import datetime -from decimal import Decimal -from django.test import TestCase -from django.utils import unittest -from rest_framework import generics, pagination, serializers, status, filters -from rest_framework.compat import django_filters +from rest_framework import exceptions, generics, pagination, serializers, status, filters from rest_framework.request import Request from rest_framework.pagination import PageLink, PAGE_BREAK from rest_framework.test import APIRequestFactory -from .models import BasicModel, FilterableItem +import pytest factory = APIRequestFactory() -# Helper function to split arguments out of an url -def split_arguments_from_url(url): - if '?' not in url: - return url - - path, args = url.split('?') - args = dict(r.split('=') for r in args.split('&')) - return path, args - - -class BasicSerializer(serializers.ModelSerializer): - class Meta: - model = BasicModel - - -class FilterableItemSerializer(serializers.ModelSerializer): - class Meta: - model = FilterableItem - - -class RootView(generics.ListCreateAPIView): +class TestPaginationIntegration: """ - Example description for OPTIONS. - """ - queryset = BasicModel.objects.all() - serializer_class = BasicSerializer - paginate_by = 10 - - -class DefaultPageSizeKwargView(generics.ListAPIView): - """ - View for testing default paginate_by_param usage - """ - queryset = BasicModel.objects.all() - serializer_class = BasicSerializer - - -class PaginateByParamView(generics.ListAPIView): - """ - View for testing custom paginate_by_param usage - """ - queryset = BasicModel.objects.all() - serializer_class = BasicSerializer - paginate_by_param = 'page_size' - - -class MaxPaginateByView(generics.ListAPIView): - """ - View for testing custom max_paginate_by usage - """ - queryset = BasicModel.objects.all() - serializer_class = BasicSerializer - paginate_by = 3 - max_paginate_by = 5 - paginate_by_param = 'page_size' - - -class IntegrationTestPagination(TestCase): - """ - Integration tests for paginated list views. + Integration tests. """ - def setUp(self): - """ - Create 26 BasicModel instances. - """ - for char in 'abcdefghijklmnopqrstuvwxyz': - BasicModel(text=char * 3).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = RootView.as_view() + def setup(self): + class PassThroughSerializer(serializers.BaseSerializer): + def to_representation(self, item): + return item - def test_get_paginated_root_view(self): - """ - GET requests to paginated ListCreateAPIView should return paginated results. - """ - request = factory.get('/') - # Note: Database queries are a `SELECT COUNT`, and `SELECT ` - with self.assertNumQueries(2): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 26) - self.assertEqual(response.data['results'], self.data[:10]) - self.assertNotEqual(response.data['next'], None) - self.assertEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['next'])) - with self.assertNumQueries(2): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 26) - self.assertEqual(response.data['results'], self.data[10:20]) - self.assertNotEqual(response.data['next'], None) - self.assertNotEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['next'])) - with self.assertNumQueries(2): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 26) - self.assertEqual(response.data['results'], self.data[20:]) - self.assertEqual(response.data['next'], None) - self.assertNotEqual(response.data['previous'], None) - - -class IntegrationTestPaginationAndFiltering(TestCase): - - def setUp(self): - """ - Create 50 FilterableItem instances. - """ - base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) - for i in range(26): - text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. - decimal = base_data[1] + i - date = base_data[2] - datetime.timedelta(days=i * 2) - FilterableItem(text=text, decimal=decimal, date=date).save() - - self.objects = FilterableItem.objects - self.data = [ - {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()} - for obj in self.objects.all() - ] - - @unittest.skipUnless(django_filters, 'django-filter not installed') - def test_get_django_filter_paginated_filtered_root_view(self): - """ - GET requests to paginated filtered ListCreateAPIView should return - paginated results. The next and previous links should preserve the - filtered parameters. - """ - class DecimalFilter(django_filters.FilterSet): - decimal = django_filters.NumberFilter(lookup_type='lt') - - class Meta: - model = FilterableItem - fields = ['text', 'decimal', 'date'] - - class FilterFieldsRootView(generics.ListCreateAPIView): - queryset = FilterableItem.objects.all() - serializer_class = FilterableItemSerializer - paginate_by = 10 - filter_class = DecimalFilter - filter_backends = (filters.DjangoFilterBackend,) - - view = FilterFieldsRootView.as_view() - - EXPECTED_NUM_QUERIES = 2 - - request = factory.get('/', {'decimal': '15.20'}) - with self.assertNumQueries(EXPECTED_NUM_QUERIES): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[:10]) - self.assertNotEqual(response.data['next'], None) - self.assertEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['next'])) - with self.assertNumQueries(EXPECTED_NUM_QUERIES): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[10:15]) - self.assertEqual(response.data['next'], None) - self.assertNotEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['previous'])) - with self.assertNumQueries(EXPECTED_NUM_QUERIES): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[:10]) - self.assertNotEqual(response.data['next'], None) - self.assertEqual(response.data['previous'], None) - - def test_get_basic_paginated_filtered_root_view(self): - """ - Same as `test_get_django_filter_paginated_filtered_root_view`, - except using a custom filter backend instead of the django-filter - backend, - """ - - class DecimalFilterBackend(filters.BaseFilterBackend): + class EvenItemsOnly(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): - return queryset.filter(decimal__lt=Decimal(request.GET['decimal'])) + return [item for item in queryset if item % 2 == 0] - class BasicFilterFieldsRootView(generics.ListCreateAPIView): - queryset = FilterableItem.objects.all() - serializer_class = FilterableItemSerializer - paginate_by = 10 - filter_backends = (DecimalFilterBackend,) + class BasicPagination(pagination.PageNumberPagination): + paginate_by = 5 + paginate_by_param = 'page_size' + max_paginate_by = 20 - view = BasicFilterFieldsRootView.as_view() + self.view = generics.ListAPIView.as_view( + serializer_class=PassThroughSerializer, + queryset=range(1, 101), + filter_backends=[EvenItemsOnly], + pagination_class=BasicPagination + ) - request = factory.get('/', {'decimal': '15.20'}) - with self.assertNumQueries(2): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[:10]) - self.assertNotEqual(response.data['next'], None) - self.assertEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['next'])) - with self.assertNumQueries(2): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[10:15]) - self.assertEqual(response.data['next'], None) - self.assertNotEqual(response.data['previous'], None) - - request = factory.get(*split_arguments_from_url(response.data['previous'])) - with self.assertNumQueries(2): - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data['count'], 15) - self.assertEqual(response.data['results'], self.data[:10]) - self.assertNotEqual(response.data['next'], None) - self.assertEqual(response.data['previous'], None) - - -class TestUnpaginated(TestCase): - """ - Tests for list views without pagination. - """ - - def setUp(self): - """ - Create 13 BasicModel instances. - """ - for i in range(13): - BasicModel(text=i).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = DefaultPageSizeKwargView.as_view() - - def test_unpaginated(self): - """ - Tests the default page size for this view. - no page size --> no limit --> no meta data - """ - request = factory.get('/') + def test_filtered_items_are_paginated(self): + request = factory.get('/', {'page': 2}) response = self.view(request) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [12, 14, 16, 18, 20], + 'previous': 'http://testserver/', + 'next': 'http://testserver/?page=3', + 'count': 50 + } + + def test_setting_page_size(self): + """ + When 'paginate_by_param' is set, the client may choose a page size. + """ + request = factory.get('/', {'page_size': 10}) + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], + 'previous': None, + 'next': 'http://testserver/?page=2&page_size=10', + 'count': 50 + } + + def test_setting_page_size_over_maximum(self): + """ + When page_size parameter exceeds maxiumum allowable, + then it should be capped to the maxiumum. + """ + request = factory.get('/', {'page_size': 1000}) + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [ + 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, + 22, 24, 26, 28, 30, 32, 34, 36, 38, 40 + ], + 'previous': None, + 'next': 'http://testserver/?page=2&page_size=1000', + 'count': 50 + } + + def test_additional_query_params_are_preserved(self): + request = factory.get('/', {'page': 2, 'filter': 'even'}) + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [12, 14, 16, 18, 20], + 'previous': 'http://testserver/?filter=even', + 'next': 'http://testserver/?filter=even&page=3', + 'count': 50 + } + + def test_404_not_found_for_invalid_page(self): + request = factory.get('/', {'page': 'invalid'}) + response = self.view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == { + 'detail': 'Invalid page "invalid": That page number is not an integer.' + } -class TestCustomPaginateByParam(TestCase): +class TestPaginationDisabledIntegration: """ - Tests for list views with default page size kwarg + Integration tests for disabled pagination. """ - def setUp(self): - """ - Create 13 BasicModel instances. - """ - for i in range(13): - BasicModel(text=i).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = PaginateByParamView.as_view() + def setup(self): + class PassThroughSerializer(serializers.BaseSerializer): + def to_representation(self, item): + return item - def test_default_page_size(self): - """ - Tests the default page size for this view. - no page size --> no limit --> no meta data - """ - request = factory.get('/') - response = self.view(request).render() - self.assertEqual(response.data, self.data) + self.view = generics.ListAPIView.as_view( + serializer_class=PassThroughSerializer, + queryset=range(1, 101), + pagination_class=None + ) - def test_paginate_by_param(self): - """ - If paginate_by_param is set, the new kwarg should limit per view requests. - """ - request = factory.get('/', {'page_size': 5}) - response = self.view(request).render() - self.assertEqual(response.data['count'], 13) - self.assertEqual(response.data['results'], self.data[:5]) + def test_unpaginated_list(self): + request = factory.get('/', {'page': 2}) + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == range(1, 101) -class TestMaxPaginateByParam(TestCase): +class TestDeprecatedStylePagination: """ - Tests for list views with max_paginate_by kwarg + Integration tests for deprecated style of setting pagination + attributes on the view. """ - def setUp(self): - """ - Create 13 BasicModel instances. - """ - for i in range(13): - BasicModel(text=i).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = MaxPaginateByView.as_view() + def setup(self): + class PassThroughSerializer(serializers.BaseSerializer): + def to_representation(self, item): + return item - def test_max_paginate_by(self): - """ - If max_paginate_by is set, it should limit page size for the view. - """ - request = factory.get('/', data={'page_size': 10}) - response = self.view(request).render() - self.assertEqual(response.data['count'], 13) - self.assertEqual(response.data['results'], self.data[:5]) + class ExampleView(generics.ListAPIView): + serializer_class = PassThroughSerializer + queryset = range(1, 101) + pagination_class = pagination.PageNumberPagination + paginate_by = 20 + page_query_param = 'page_number' - def test_max_paginate_by_without_page_size_param(self): - """ - If max_paginate_by is set, but client does not specifiy page_size, - standard `paginate_by` behavior should be used. - """ - request = factory.get('/') - response = self.view(request).render() - self.assertEqual(response.data['results'], self.data[:3]) + self.view = ExampleView.as_view() + + def test_paginate_by_attribute_on_view(self): + request = factory.get('/?page_number=2') + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [ + 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 + ], + 'previous': 'http://testserver/', + 'next': 'http://testserver/?page_number=3', + 'count': 100 + } + + +class TestPageNumberPagination: + """ + Unit tests for `pagination.PageNumberPagination`. + """ + + def setup(self): + class ExamplePagination(pagination.PageNumberPagination): + paginate_by = 5 + self.pagination = ExamplePagination() + self.queryset = range(1, 101) + + def paginate_queryset(self, request): + return list(self.pagination.paginate_queryset(self.queryset, request)) + + def get_paginated_content(self, queryset): + response = self.pagination.get_paginated_response(queryset) + return response.data + + def get_html_context(self): + return self.pagination.get_html_context() + + def test_no_page_number(self): + request = Request(factory.get('/')) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [1, 2, 3, 4, 5] + assert content == { + 'results': [1, 2, 3, 4, 5], + 'previous': None, + 'next': 'http://testserver/?page=2', + 'count': 100 + } + assert context == { + 'previous_url': None, + 'next_url': 'http://testserver/?page=2', + 'page_links': [ + PageLink('http://testserver/', 1, True, False), + PageLink('http://testserver/?page=2', 2, False, False), + PageLink('http://testserver/?page=3', 3, False, False), + PAGE_BREAK, + PageLink('http://testserver/?page=20', 20, False, False), + ] + } + assert self.pagination.display_page_controls + assert isinstance(self.pagination.to_html(), type('')) + + def test_second_page(self): + request = Request(factory.get('/', {'page': 2})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [6, 7, 8, 9, 10] + assert content == { + 'results': [6, 7, 8, 9, 10], + 'previous': 'http://testserver/', + 'next': 'http://testserver/?page=3', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/', + 'next_url': 'http://testserver/?page=3', + 'page_links': [ + PageLink('http://testserver/', 1, False, False), + PageLink('http://testserver/?page=2', 2, True, False), + PageLink('http://testserver/?page=3', 3, False, False), + PAGE_BREAK, + PageLink('http://testserver/?page=20', 20, False, False), + ] + } + + def test_last_page(self): + request = Request(factory.get('/', {'page': 'last'})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [96, 97, 98, 99, 100] + assert content == { + 'results': [96, 97, 98, 99, 100], + 'previous': 'http://testserver/?page=19', + 'next': None, + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?page=19', + 'next_url': None, + 'page_links': [ + PageLink('http://testserver/', 1, False, False), + PAGE_BREAK, + PageLink('http://testserver/?page=18', 18, False, False), + PageLink('http://testserver/?page=19', 19, False, False), + PageLink('http://testserver/?page=20', 20, True, False), + ] + } + + def test_invalid_page(self): + request = Request(factory.get('/', {'page': 'invalid'})) + with pytest.raises(exceptions.NotFound): + self.paginate_queryset(request) class TestLimitOffset: + """ + Unit tests for `pagination.LimitOffsetPagination`. + """ + def setup(self): - self.pagination = pagination.LimitOffsetPagination() + class ExamplePagination(pagination.LimitOffsetPagination): + default_limit = 10 + self.pagination = ExamplePagination() self.queryset = range(1, 101) def paginate_queryset(self, request): @@ -379,6 +300,37 @@ class TestLimitOffset: PageLink('http://testserver/?limit=5&offset=95', 20, False, False), ] } + assert self.pagination.display_page_controls + assert isinstance(self.pagination.to_html(), type('')) + + def test_single_offset(self): + """ + When the offset is not a multiple of the limit we get some edge cases: + * The first page should still be offset zero. + * We may end up displaying an extra page in the pagination control. + """ + request = Request(factory.get('/', {'limit': 5, 'offset': 1})) + queryset = self.paginate_queryset(request) + content = self.get_paginated_content(queryset) + context = self.get_html_context() + assert queryset == [2, 3, 4, 5, 6] + assert content == { + 'results': [2, 3, 4, 5, 6], + 'previous': 'http://testserver/?limit=5', + 'next': 'http://testserver/?limit=5&offset=6', + 'count': 100 + } + assert context == { + 'previous_url': 'http://testserver/?limit=5', + 'next_url': 'http://testserver/?limit=5&offset=6', + 'page_links': [ + PageLink('http://testserver/?limit=5', 1, False, False), + PageLink('http://testserver/?limit=5&offset=1', 2, True, False), + PageLink('http://testserver/?limit=5&offset=6', 3, False, False), + PAGE_BREAK, + PageLink('http://testserver/?limit=5&offset=96', 21, False, False), + ] + } def test_first_offset(self): request = Request(factory.get('/', {'limit': 5, 'offset': 5})) @@ -452,3 +404,72 @@ class TestLimitOffset: PageLink('http://testserver/?limit=5&offset=95', 20, True, False), ] } + + def test_invalid_offset(self): + """ + An invalid offset query param should be treated as 0. + """ + request = Request(factory.get('/', {'limit': 5, 'offset': 'invalid'})) + queryset = self.paginate_queryset(request) + assert queryset == [1, 2, 3, 4, 5] + + def test_invalid_limit(self): + """ + An invalid limit query param should be ignored in favor of the default. + """ + request = Request(factory.get('/', {'limit': 'invalid', 'offset': 0})) + queryset = self.paginate_queryset(request) + assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + +def test_get_displayed_page_numbers(): + """ + Test our contextual page display function. + + This determines which pages to display in a pagination control, + given the current page and the last page. + """ + displayed_page_numbers = pagination._get_displayed_page_numbers + + # At five pages or less, all pages are displayed, always. + assert displayed_page_numbers(1, 5) == [1, 2, 3, 4, 5] + assert displayed_page_numbers(2, 5) == [1, 2, 3, 4, 5] + assert displayed_page_numbers(3, 5) == [1, 2, 3, 4, 5] + assert displayed_page_numbers(4, 5) == [1, 2, 3, 4, 5] + assert displayed_page_numbers(5, 5) == [1, 2, 3, 4, 5] + + # Between six and either pages we may have a single page break. + assert displayed_page_numbers(1, 6) == [1, 2, 3, None, 6] + assert displayed_page_numbers(2, 6) == [1, 2, 3, None, 6] + assert displayed_page_numbers(3, 6) == [1, 2, 3, 4, 5, 6] + assert displayed_page_numbers(4, 6) == [1, 2, 3, 4, 5, 6] + assert displayed_page_numbers(5, 6) == [1, None, 4, 5, 6] + assert displayed_page_numbers(6, 6) == [1, None, 4, 5, 6] + + assert displayed_page_numbers(1, 7) == [1, 2, 3, None, 7] + assert displayed_page_numbers(2, 7) == [1, 2, 3, None, 7] + assert displayed_page_numbers(3, 7) == [1, 2, 3, 4, None, 7] + assert displayed_page_numbers(4, 7) == [1, 2, 3, 4, 5, 6, 7] + assert displayed_page_numbers(5, 7) == [1, None, 4, 5, 6, 7] + assert displayed_page_numbers(6, 7) == [1, None, 5, 6, 7] + assert displayed_page_numbers(7, 7) == [1, None, 5, 6, 7] + + assert displayed_page_numbers(1, 8) == [1, 2, 3, None, 8] + assert displayed_page_numbers(2, 8) == [1, 2, 3, None, 8] + assert displayed_page_numbers(3, 8) == [1, 2, 3, 4, None, 8] + assert displayed_page_numbers(4, 8) == [1, 2, 3, 4, 5, None, 8] + assert displayed_page_numbers(5, 8) == [1, None, 4, 5, 6, 7, 8] + assert displayed_page_numbers(6, 8) == [1, None, 5, 6, 7, 8] + assert displayed_page_numbers(7, 8) == [1, None, 6, 7, 8] + assert displayed_page_numbers(8, 8) == [1, None, 6, 7, 8] + + # At nine or more pages we may have two page breaks, one on each side. + assert displayed_page_numbers(1, 9) == [1, 2, 3, None, 9] + assert displayed_page_numbers(2, 9) == [1, 2, 3, None, 9] + assert displayed_page_numbers(3, 9) == [1, 2, 3, 4, None, 9] + assert displayed_page_numbers(4, 9) == [1, 2, 3, 4, 5, None, 9] + assert displayed_page_numbers(5, 9) == [1, None, 4, 5, 6, None, 9] + assert displayed_page_numbers(6, 9) == [1, None, 5, 6, 7, 8, 9] + assert displayed_page_numbers(7, 9) == [1, None, 6, 7, 8, 9] + assert displayed_page_numbers(8, 9) == [1, None, 7, 8, 9] + assert displayed_page_numbers(9, 9) == [1, None, 7, 8, 9] From 86d2774cf30351fd4174e97501532056ed0d8f95 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Jan 2015 20:30:46 +0000 Subject: [PATCH 069/301] Fix compat issues --- rest_framework/pagination.py | 8 ++--- rest_framework/templatetags/rest_framework.py | 34 ++----------------- rest_framework/utils/urls.py | 25 ++++++++++++++ tests/test_pagination.py | 4 +-- 4 files changed, 34 insertions(+), 37 deletions(-) create mode 100644 rest_framework/utils/urls.py diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index c5a364f0a..1b7524c6c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -12,7 +12,7 @@ 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 ( +from rest_framework.utils.urls import ( replace_query_param, remove_query_param ) @@ -34,8 +34,8 @@ def _divide_with_ceil(a, b): Returns 'a' divded by 'b', with any remainder rounded up. """ if a % b: - return (a / b) + 1 - return a / b + return (a // b) + 1 + return a // b def _get_count(queryset): @@ -70,7 +70,7 @@ def _get_displayed_page_numbers(current, final): assert final >= current if final <= 5: - return range(1, final + 1) + return list(range(1, final + 1)) # We always include the first two pages, last two pages, and # two pages either side of the current page. diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index bf159d8b1..a969836fd 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -1,41 +1,19 @@ from __future__ import unicode_literals, absolute_import from django import template from django.core.urlresolvers import reverse, NoReverseMatch -from django.http import QueryDict from django.utils import six -from django.utils.six.moves.urllib import parse as urlparse from django.utils.encoding import iri_to_uri, force_text from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe from django.utils.html import smart_urlquote from rest_framework.renderers import HTMLFormRenderer +from rest_framework.utils.urls import replace_query_param import re register = template.Library() - -def replace_query_param(url, key, val): - """ - 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[key] = val - query = query_dict.urlencode() - 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)) +# Regex for adding classes to html snippets +class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') @register.simple_tag @@ -43,12 +21,6 @@ def get_pagination_html(pager): return pager.to_html() -# Regex for adding classes to html snippets -class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') - - -# And the template tags themselves... - @register.simple_tag def render_field(field, style=None): style = style or {} diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py new file mode 100644 index 000000000..880ef9ed7 --- /dev/null +++ b/rest_framework/utils/urls.py @@ -0,0 +1,25 @@ +from django.utils.six.moves.urllib import parse as urlparse + + +def replace_query_param(url, key, val): + """ + 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 = urlparse.parse_qs(query) + query_dict[key] = [val] + query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + + +def remove_query_param(url, key): + """ + Given a URL and a key/val pair, remove an item in the query + parameters of the URL, and return the new URL. + """ + (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url) + query_dict = urlparse.parse_qs(query) + query_dict.pop(key, None) + query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True) + return urlparse.urlunsplit((scheme, netloc, path, query, fragment)) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index b3436b359..7cc923472 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -117,7 +117,7 @@ class TestPaginationDisabledIntegration: request = factory.get('/', {'page': 2}) response = self.view(request) assert response.status_code == status.HTTP_200_OK - assert response.data == range(1, 101) + assert response.data == list(range(1, 101)) class TestDeprecatedStylePagination: @@ -268,7 +268,7 @@ class TestLimitOffset: self.queryset = range(1, 101) def paginate_queryset(self, request): - return self.pagination.paginate_queryset(self.queryset, request) + return list(self.pagination.paginate_queryset(self.queryset, request)) def get_paginated_content(self, queryset): response = self.pagination.get_paginated_response(queryset) From 4919492582547d227a22852ad2339fa73739cc94 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 17 Jan 2015 00:10:43 +0000 Subject: [PATCH 070/301] First pass at cursor pagination --- rest_framework/pagination.py | 51 +++++++++++++++++++++ tests/test_pagination.py | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1b7524c6c..89d6f9f4f 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -3,10 +3,12 @@ Pagination serializers determine the structure of the output that should be used for paginated responses. """ from __future__ import unicode_literals +from base64 import b64encode, b64decode 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.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext as _ from rest_framework.compat import OrderedDict from rest_framework.exceptions import NotFound @@ -377,3 +379,52 @@ class LimitOffsetPagination(BasePagination): template = loader.get_template(self.template) context = Context(self.get_html_context()) return template.render(context) + + +class CursorPagination(BasePagination): + # reverse + # limit + # multiple orderings + cursor_query_param = 'cursor' + page_size = 5 + + def paginate_queryset(self, queryset, request, view=None): + self.base_url = request.build_absolute_uri() + self.ordering = self.get_ordering() + encoded = request.query_params.get(self.cursor_query_param) + + if encoded is None: + cursor = None + else: + cursor = self.decode_cursor(encoded, self.ordering) + + if cursor is not None: + kwargs = {self.ordering + '__gt': cursor} + queryset = queryset.filter(**kwargs) + + results = list(queryset[:self.page_size + 1]) + self.page = results[:self.page_size] + self.has_next = len(results) > len(self.page) + return self.page + + def get_next_link(self): + if not self.has_next: + return None + last_item = self.page[-1] + cursor = self.get_cursor_from_instance(last_item, self.ordering) + encoded = self.encode_cursor(cursor, self.ordering) + return replace_query_param(self.base_url, self.cursor_query_param, encoded) + + def get_ordering(self): + return 'created' + + def get_cursor_from_instance(self, instance, ordering): + return getattr(instance, ordering) + + def decode_cursor(self, encoded, ordering): + items = urlparse.parse_qs(b64decode(encoded)) + return items.get(ordering)[0] + + def encode_cursor(self, cursor, ordering): + items = [(ordering, cursor)] + return b64encode(urlparse.urlencode(items, doseq=True)) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 7cc923472..7f18b446b 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -422,6 +422,94 @@ class TestLimitOffset: assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] +class TestCursorPagination: + """ + Unit tests for `pagination.CursorPagination`. + """ + + def setup(self): + class MockObject(object): + def __init__(self, idx): + self.created = idx + + class MockQuerySet(object): + def __init__(self, items): + self.items = items + + def filter(self, created__gt): + return [ + item for item in self.items + if item.created > int(created__gt) + ] + + def __getitem__(self, sliced): + return self.items[sliced] + + self.pagination = pagination.CursorPagination() + self.queryset = MockQuerySet( + [MockObject(idx) for idx in range(1, 21)] + ) + + def paginate_queryset(self, request): + return list(self.pagination.paginate_queryset(self.queryset, request)) + + # def get_paginated_content(self, queryset): + # response = self.pagination.get_paginated_response(queryset) + # return response.data + + # def get_html_context(self): + # return self.pagination.get_html_context() + + def test_following_cursor(self): + request = Request(factory.get('/')) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 2, 3, 4, 5] + + next_url = self.pagination.get_next_link() + assert next_url + + request = Request(factory.get(next_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [6, 7, 8, 9, 10] + + next_url = self.pagination.get_next_link() + assert next_url + + request = Request(factory.get(next_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [11, 12, 13, 14, 15] + + next_url = self.pagination.get_next_link() + assert next_url + + request = Request(factory.get(next_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [16, 17, 18, 19, 20] + + next_url = self.pagination.get_next_link() + assert next_url is None + + # assert content == { + # 'results': [1, 2, 3, 4, 5], + # 'previous': None, + # 'next': 'http://testserver/?limit=5&offset=5', + # 'count': 100 + # } + # assert context == { + # 'previous_url': None, + # 'next_url': 'http://testserver/?limit=5&offset=5', + # 'page_links': [ + # PageLink('http://testserver/?limit=5', 1, True, False), + # PageLink('http://testserver/?limit=5&offset=5', 2, False, False), + # PageLink('http://testserver/?limit=5&offset=10', 3, False, False), + # PAGE_BREAK, + # PageLink('http://testserver/?limit=5&offset=95', 20, False, False), + # ] + # } + # assert self.pagination.display_page_controls + # assert isinstance(self.pagination.to_html(), type('')) + + def test_get_displayed_page_numbers(): """ Test our contextual page display function. From 492f3c410d3a91a3f37218e93485a693d9078000 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 17 Jan 2015 00:59:02 +0000 Subject: [PATCH 071/301] Cleaning up cursor implementation --- rest_framework/pagination.py | 51 +++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 89d6f9f4f..3984da13e 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -381,10 +381,33 @@ class LimitOffsetPagination(BasePagination): return template.render(context) +Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) + + +def decode_cursor(encoded): + tokens = urlparse.parse_qs(b64decode(encoded)) + try: + offset = int(tokens['offset'][0]) + reverse = bool(int(tokens['reverse'][0])) + position = tokens['position'][0] + except (TypeError, ValueError): + return None + + return Cursor(offset=offset, reverse=reverse, position=position) + + +def encode_cursor(cursor): + tokens = { + 'offset': str(cursor.offset), + 'reverse': '1' if cursor.reverse else '0', + 'position': cursor.position + } + return b64encode(urlparse.urlencode(tokens, doseq=True)) + + class CursorPagination(BasePagination): # reverse # limit - # multiple orderings cursor_query_param = 'cursor' page_size = 5 @@ -396,10 +419,11 @@ class CursorPagination(BasePagination): if encoded is None: cursor = None else: - cursor = self.decode_cursor(encoded, self.ordering) + cursor = decode_cursor(encoded) + # TODO: Invalid cursors should 404 if cursor is not None: - kwargs = {self.ordering + '__gt': cursor} + kwargs = {self.ordering + '__gt': cursor.position} queryset = queryset.filter(**kwargs) results = list(queryset[:self.page_size + 1]) @@ -411,20 +435,21 @@ class CursorPagination(BasePagination): if not self.has_next: return None last_item = self.page[-1] - cursor = self.get_cursor_from_instance(last_item, self.ordering) - encoded = self.encode_cursor(cursor, self.ordering) + position = self.get_position_from_instance(last_item, self.ordering) + cursor = Cursor(offset=0, reverse=False, position=position) + encoded = encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) def get_ordering(self): return 'created' - def get_cursor_from_instance(self, instance, ordering): - return getattr(instance, ordering) + def get_position_from_instance(self, instance, ordering): + return str(getattr(instance, ordering)) - def decode_cursor(self, encoded, ordering): - items = urlparse.parse_qs(b64decode(encoded)) - return items.get(ordering)[0] + # def decode_cursor(self, encoded, ordering): + # items = urlparse.parse_qs(b64decode(encoded)) + # return items.get(ordering)[0] - def encode_cursor(self, cursor, ordering): - items = [(ordering, cursor)] - return b64encode(urlparse.urlencode(items, doseq=True)) + # def encode_cursor(self, cursor, ordering): + # items = [(ordering, cursor)] + # return b64encode(urlparse.urlencode(items, doseq=True)) From dbb684117f6fe0f9c34f98d5e914fc106090cdbc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Jan 2015 09:24:42 +0000 Subject: [PATCH 072/301] Add offset support for cursor pagination --- rest_framework/pagination.py | 67 ++++++++++++++++++++++++++---------- tests/test_pagination.py | 64 ++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 22 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 3984da13e..f56f55ce1 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -1,3 +1,4 @@ +# coding: utf-8 """ Pagination serializers determine the structure of the output that should be used for paginated responses. @@ -385,7 +386,7 @@ Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) def decode_cursor(encoded): - tokens = urlparse.parse_qs(b64decode(encoded)) + tokens = urlparse.parse_qs(b64decode(encoded), keep_blank_values=True) try: offset = int(tokens['offset'][0]) reverse = bool(int(tokens['reverse'][0])) @@ -406,8 +407,7 @@ def encode_cursor(cursor): class CursorPagination(BasePagination): - # reverse - # limit + # TODO: reverse cursors cursor_query_param = 'cursor' page_size = 5 @@ -417,26 +417,63 @@ class CursorPagination(BasePagination): encoded = request.query_params.get(self.cursor_query_param) if encoded is None: - cursor = None + self.cursor = None else: - cursor = decode_cursor(encoded) + self.cursor = decode_cursor(encoded) # TODO: Invalid cursors should 404 - if cursor is not None: - kwargs = {self.ordering + '__gt': cursor.position} + if self.cursor is not None and self.cursor.position != '': + kwargs = {self.ordering + '__gt': self.cursor.position} queryset = queryset.filter(**kwargs) - results = list(queryset[:self.page_size + 1]) + # The offset is used in order to deal with cases where we have + # items with an identical position. This allows the cursors + # to gracefully deal with non-unique fields as the ordering. + offset = 0 if (self.cursor is None) else self.cursor.offset + + # We fetch an extra item in order to determine if there is a next page. + results = list(queryset[offset:offset + self.page_size + 1]) self.page = results[:self.page_size] self.has_next = len(results) > len(self.page) + self.next_item = results[-1] if self.has_next else None return self.page def get_next_link(self): if not self.has_next: return None - last_item = self.page[-1] - position = self.get_position_from_instance(last_item, self.ordering) - cursor = Cursor(offset=0, reverse=False, position=position) + + compare = self.get_position_from_instance(self.next_item, self.ordering) + offset = 0 + for item in reversed(self.page): + position = self.get_position_from_instance(item, self.ordering) + if position != compare: + # The item in this position and the item following it + # have different positions. We can use this position as + # our marker. + break + + # The item in this postion has the same position as the item + # following it, we can't use it as a marker position, so increment + # the offset and keep seeking to the previous item. + compare = position + offset += 1 + + else: + if self.cursor is None: + # There were no unique positions in the page, and we were + # on the first page, ie. there was no existing cursor. + # Our cursor will have an offset equal to the page size, + # but no position to filter against yet. + offset = self.page_size + position = '' + else: + # There were no unique positions in the page. + # Use the position from the existing cursor and increment + # it's offset by the page size. + offset = self.cursor.offset + self.page_size + position = self.cursor.position + + cursor = Cursor(offset=offset, reverse=False, position=position) encoded = encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) @@ -445,11 +482,3 @@ class CursorPagination(BasePagination): def get_position_from_instance(self, instance, ordering): return str(getattr(instance, ordering)) - - # def decode_cursor(self, encoded, ordering): - # items = urlparse.parse_qs(b64decode(encoded)) - # return items.get(ordering)[0] - - # def encode_cursor(self, cursor, ordering): - # items = [(ordering, cursor)] - # return b64encode(urlparse.urlencode(items, doseq=True)) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 7f18b446b..f04079a72 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -447,7 +447,7 @@ class TestCursorPagination: self.pagination = pagination.CursorPagination() self.queryset = MockQuerySet( - [MockObject(idx) for idx in range(1, 21)] + [MockObject(idx) for idx in range(1, 16)] ) def paginate_queryset(self, request): @@ -479,16 +479,74 @@ class TestCursorPagination: queryset = self.paginate_queryset(request) assert [item.created for item in queryset] == [11, 12, 13, 14, 15] + next_url = self.pagination.get_next_link() + assert next_url is None + + +class TestCrazyCursorPagination: + """ + Unit tests for `pagination.CursorPagination`. + """ + + def setup(self): + class MockObject(object): + def __init__(self, idx): + self.created = idx + + class MockQuerySet(object): + def __init__(self, items): + self.items = items + + def filter(self, created__gt): + return [ + item for item in self.items + if item.created > int(created__gt) + ] + + def __getitem__(self, sliced): + return self.items[sliced] + + self.pagination = pagination.CursorPagination() + self.queryset = MockQuerySet([ + MockObject(idx) for idx in [ + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 2, 3, 4, + 5, 6, 7, 8, 9 + ] + ]) + + def paginate_queryset(self, request): + return list(self.pagination.paginate_queryset(self.queryset, request)) + + def test_following_cursor_identical_items(self): + request = Request(factory.get('/')) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + next_url = self.pagination.get_next_link() assert next_url request = Request(factory.get(next_url)) queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [16, 17, 18, 19, 20] + assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + + next_url = self.pagination.get_next_link() + assert next_url + + request = Request(factory.get(next_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 1, 2, 3, 4] + + next_url = self.pagination.get_next_link() + assert next_url + + request = Request(factory.get(next_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [5, 6, 7, 8, 9] next_url = self.pagination.get_next_link() assert next_url is None - # assert content == { # 'results': [1, 2, 3, 4, 5], # 'previous': None, From 4f3c3a06cfc0ea2dfbf46da2d98546664343ce93 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Jan 2015 14:41:10 +0000 Subject: [PATCH 073/301] Drop trailing whitespace on indented JSON output. Closes #2429. --- rest_framework/compat.py | 2 ++ rest_framework/renderers.py | 8 ++++++-- tests/test_renderers.py | 24 +++++++++++++++++++++++- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7241da279..ea3429942 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -227,6 +227,8 @@ except ImportError: if six.PY3: SHORT_SEPARATORS = (',', ':') LONG_SEPARATORS = (', ', ': ') + INDENT_SEPARATORS = (',', ': ') else: SHORT_SEPARATORS = (b',', b':') LONG_SEPARATORS = (b', ', b': ') + INDENT_SEPARATORS = (b',', b': ') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c46b049f..7af03c674 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -18,7 +18,7 @@ from django.template import Context, RequestContext, loader, Template from django.test.client import encode_multipart from django.utils import six from rest_framework import exceptions, serializers, status, VERSION -from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS +from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, INDENT_SEPARATORS from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.request import is_form_media_type, override_method @@ -87,7 +87,11 @@ class JSONRenderer(BaseRenderer): renderer_context = renderer_context or {} indent = self.get_indent(accepted_media_type, renderer_context) - separators = SHORT_SEPARATORS if (indent is None and self.compact) else LONG_SEPARATORS + + if indent is None: + separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS + else: + separators = INDENT_SEPARATORS ret = json.dumps( data, cls=self.encoder_class, diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 7b78f7baf..3e64d8fec 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals - from django.conf.urls import patterns, url, include from django.core.cache import cache from django.db import models @@ -8,6 +7,7 @@ from django.test import TestCase from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework import status, permissions +from rest_framework.compat import OrderedDict from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, BrowsableAPIRenderer @@ -489,3 +489,25 @@ class CacheRenderTest(TestCase): cached_resp = cache.get(self.cache_key) self.assertIsInstance(cached_resp, Response) self.assertEqual(cached_resp.content, resp.content) + + +class TestJSONIndentationStyles: + def test_indented(self): + renderer = JSONRenderer() + data = OrderedDict([('a', 1), ('b', 2)]) + assert renderer.render(data) == b'{"a":1,"b":2}' + + def test_compact(self): + renderer = JSONRenderer() + data = OrderedDict([('a', 1), ('b', 2)]) + context = {'indent': 4} + assert ( + renderer.render(data, renderer_context=context) == + b'{\n "a": 1,\n "b": 2\n}' + ) + + def test_long_form(self): + renderer = JSONRenderer() + renderer.compact = False + data = OrderedDict([('a', 1), ('b', 2)]) + assert renderer.render(data) == b'{"a": 1, "b": 2}' From 3cc39ffbceffc5fdbb511d9a10e7732329e8baa4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 19 Jan 2015 15:22:38 +0000 Subject: [PATCH 074/301] NotImplemented -> NotImplementedError --- rest_framework/pagination.py | 6 +++--- rest_framework/versioning.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1b7524c6c..55c173df4 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -132,13 +132,13 @@ class BasePagination(object): display_page_controls = False def paginate_queryset(self, queryset, request, view=None): # pragma: no cover - raise NotImplemented('paginate_queryset() must be implemented.') + raise NotImplementedError('paginate_queryset() must be implemented.') def get_paginated_response(self, data): # pragma: no cover - raise NotImplemented('get_paginated_response() must be implemented.') + raise NotImplementedError('get_paginated_response() must be implemented.') def to_html(self): # pragma: no cover - raise NotImplemented('to_html() must be implemented to display page controls.') + raise NotImplementedError('to_html() must be implemented to display page controls.') class PageNumberPagination(BasePagination): diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index e31c71e9b..a07b629fe 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -17,7 +17,7 @@ class BaseVersioning(object): def determine_version(self, request, *args, **kwargs): msg = '{cls}.determine_version() must be implemented.' - raise NotImplemented(msg.format( + raise NotImplementedError(msg.format( cls=self.__class__.__name__ )) From da6ef3d0b0f3a8e688524bbd446d4350a74fd05a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 13:03:37 +0000 Subject: [PATCH 075/301] Allow missing fields option for inherited serializers. Closes #2388. --- rest_framework/compat.py | 2 +- rest_framework/serializers.py | 32 +++++++------ rest_framework/utils/serializer_helpers.py | 3 ++ tests/test_model_serializer.py | 52 +++++++++++++++++----- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 17814136b..766afaec2 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -20,7 +20,7 @@ def unicode_repr(instance): # Get the repr of an instance, but ensure it is a unicode string # on both python 3 (already the case) and 2 (not the case). if six.PY2: - repr(instance).decode('utf-8') + return repr(instance).decode('utf-8') return repr(instance) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e373cd107..6320a0751 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -253,7 +253,7 @@ class SerializerMetaclass(type): # If this class is subclassing another Serializer, add that Serializer's # fields. Note that we loop over the bases in *reverse*. This is necessary # in order to maintain the correct order of fields. - for base in bases[::-1]: + for base in reversed(bases): if hasattr(base, '_declared_fields'): fields = list(base._declared_fields.items()) + fields @@ -880,8 +880,8 @@ class ModelSerializer(Serializer): # 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: + # Use the default set of field names if none is supplied explicitly. fields = self._get_default_field_names(declared_fields, info) exclude = getattr(self.Meta, 'exclude', None) if exclude is not None: @@ -891,6 +891,23 @@ class ModelSerializer(Serializer): field_name ) fields.remove(field_name) + else: + # Check that any fields declared on the class are + # also explicitly included in `Meta.fields`. + + # Note that we ignore any fields that were declared on a parent + # class, in order to support only including a subset of fields + # when subclassing serializers. + declared_field_names = set(declared_fields.keys()) + for cls in self.__class__.__bases__: + declared_field_names -= set(getattr(cls, '_declared_fields', [])) + + missing_fields = declared_field_names - set(fields) + assert not missing_fields, ( + 'Field `%s` has been declared on serializer `%s`, but ' + 'is missing from `Meta.fields`.' % + (list(missing_fields)[0], self.__class__.__name__) + ) # 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 +1041,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): diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index f99606039..ab0578620 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -105,3 +105,6 @@ class BindingDict(collections.MutableMapping): def __len__(self): return len(self.fields) + + def __repr__(self): + return dict.__repr__(self.fields) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index ee556dbcb..247b309a1 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -5,11 +5,14 @@ shortcuts for automatically creating serializers based on a given model class. These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ +from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator from django.db import models from django.test import TestCase +from django.utils import six from rest_framework import serializers +from rest_framework.compat import unicode_repr def dedent(blocktext): @@ -124,7 +127,7 @@ class TestRegularFieldMappings(TestCase): url_field = URLField(max_length=100) custom_field = ModelField(model_field=) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_field_options(self): class TestSerializer(serializers.ModelSerializer): @@ -142,7 +145,14 @@ class TestRegularFieldMappings(TestCase): descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')]) """) - self.assertEqual(repr(TestSerializer()), expected) + if six.PY2: + # This particular case is too awkward to resolve fully across + # both py2 and py3. + expected = expected.replace( + "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')", + "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')" + ) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_method_field(self): """ @@ -221,7 +231,7 @@ 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 ' @@ -229,6 +239,26 @@ class TestRegularFieldMappings(TestCase): ) assert str(excinfo.exception) == expected + def test_missing_superclass_field(self): + """ + Fields that have been declared on a parent of the serializer class may + be excluded from the `Meta.fields` option. + """ + class TestSerializer(serializers.ModelSerializer): + missing = serializers.ReadOnlyField() + + class Meta: + model = RegularFieldsModel + + class ChildSerializer(TestSerializer): + missing = serializers.ReadOnlyField() + + class Meta: + model = RegularFieldsModel + fields = ('auto_field',) + + ChildSerializer().fields + # Tests for relational field mappings. # ------------------------------------ @@ -276,7 +306,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all()) through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_nested_relations(self): class TestSerializer(serializers.ModelSerializer): @@ -300,7 +330,7 @@ class TestRelationalFieldMappings(TestCase): id = IntegerField(label='ID', read_only=True) name = CharField(max_length=100) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -315,7 +345,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail') through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail') """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_nested_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -339,7 +369,7 @@ class TestRelationalFieldMappings(TestCase): url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail') name = CharField(max_length=100) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_foreign_key(self): class TestSerializer(serializers.ModelSerializer): @@ -353,7 +383,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_one_to_one(self): class TestSerializer(serializers.ModelSerializer): @@ -367,7 +397,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_many_to_many(self): class TestSerializer(serializers.ModelSerializer): @@ -381,7 +411,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_through(self): class TestSerializer(serializers.ModelSerializer): @@ -395,7 +425,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) class TestIntegration(TestCase): From e59b3d1718de549d0e165d03aeea1488ddfe20ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 14:18:13 +0000 Subject: [PATCH 076/301] Make ReturnDict cachable. Closes #2360. --- rest_framework/utils/serializer_helpers.py | 10 ++++++++++ tests/test_serializer.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index ab0578620..87bb3ac08 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -19,6 +19,11 @@ class ReturnDict(OrderedDict): def __repr__(self): return dict.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (dict, (dict(self),)) + class ReturnList(list): """ @@ -33,6 +38,11 @@ class ReturnList(list): def __repr__(self): return list.__repr__(self) + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (list, (list(self),)) + class BoundField(object): """ diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 68bbbe983..b7a0484bc 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from .utils import MockObject from rest_framework import serializers from rest_framework.compat import unicode_repr +import pickle import pytest @@ -278,3 +279,19 @@ class TestNotRequiredOutput: serializer = ExampleSerializer(instance) with pytest.raises(AttributeError): serializer.data + + +class TestCacheSerializerData: + def test_cache_serializer_data(self): + """ + Caching serializer data with pickle will drop the serializer info, + but does preserve the data itself. + """ + class ExampleSerializer(serializers.Serializer): + field1 = serializers.CharField() + field2 = serializers.CharField() + + serializer = ExampleSerializer({'field1': 'a', 'field2': 'b'}) + pickled = pickle.dumps(serializer.data) + data = pickle.loads(pickled) + assert data == {'field1': 'a', 'field2': 'b'} From 4cf03e30ff765dda2899048725da4d85ebd8af52 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Jan 2015 14:26:25 +0000 Subject: [PATCH 077/301] Do not render HTML output for hidden fields. Closes #2410. --- rest_framework/renderers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba6c9cc15..584332e66 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -410,6 +410,9 @@ class HTMLFormRenderer(BaseRenderer): }) def render_field(self, field, parent_style): + if isinstance(field, serializers.HiddenField): + return '' + style = dict(self.default_style[field]) style.update(field.style) if 'template_pack' not in style: From a7567efa8d6fd008ba0a48f0e8fa7028703af386 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Jan 2015 19:26:57 +0100 Subject: [PATCH 078/301] Use compact traceback for errors reporting. --- runtests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runtests.py b/runtests.py index abf15a623..0008bfae5 100755 --- a/runtests.py +++ b/runtests.py @@ -8,8 +8,8 @@ import subprocess PYTEST_ARGS = { - 'default': ['tests'], - 'fast': ['tests', '-q'], + 'default': ['tests', '--tb=short'], + 'fast': ['tests', '--tb=short', '-q'], } FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] From 857185cf07bb539083a90bc75a6dd951da8e2206 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Jan 2015 19:29:40 +0100 Subject: [PATCH 079/301] Workaround Django issue 24198. --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6320a0751..b1474562b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import FieldDoesNotExist, Field from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -939,6 +939,9 @@ class ModelSerializer(Serializer): except FieldDoesNotExist: continue + if not isinstance(model_field, Field): + continue + # Include each of the `unique_for_*` field names. unique_constraint_names |= set([ model_field.unique_for_date, From 15f797fd3ec61947aaecc05e6fd040e1e3e8776a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 21 Jan 2015 19:46:31 +0100 Subject: [PATCH 080/301] Owned by import * --- 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 b1474562b..cf797bdcb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist, Field +from django.db.models.fields import FieldDoesNotExist, Field as DjangoField from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -939,7 +939,7 @@ class ModelSerializer(Serializer): except FieldDoesNotExist: continue - if not isinstance(model_field, Field): + if not isinstance(model_field, DjangoField): continue # Include each of the `unique_for_*` field names. From 6e471ad8f41dda11365080ca583a0ccbf37de55e Mon Sep 17 00:00:00 2001 From: Duncan Maitland Date: Thu, 22 Jan 2015 18:29:20 +1100 Subject: [PATCH 081/301] fix link to Django CSRF docs --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1222dbf04..0d53de70a 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -427,7 +427,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [oauth]: http://oauth.net/2/ [permission]: permissions.md [throttling]: throttling.md -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax [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 From cae9528c54ea13863ea056d40168e8d8df68b276 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 10:28:19 +0000 Subject: [PATCH 082/301] Add support for reverse cursors --- rest_framework/pagination.py | 126 +++++++++++++++++++++++++++++------ tests/test_pagination.py | 6 ++ 2 files changed, 112 insertions(+), 20 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 5482788a3..9e22a8bf3 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -407,45 +407,84 @@ def encode_cursor(cursor): class CursorPagination(BasePagination): - # TODO: reverse cursors + # TODO: handle queries with '' as a legitimate position cursor_query_param = 'cursor' page_size = 5 def paginate_queryset(self, queryset, request, view=None): self.base_url = request.build_absolute_uri() self.ordering = self.get_ordering() - encoded = request.query_params.get(self.cursor_query_param) + # Determine if we have a cursor, and if so then decode it. + encoded = request.query_params.get(self.cursor_query_param) if encoded is None: self.cursor = None + (offset, reverse, current_position) = (0, False, '') else: self.cursor = decode_cursor(encoded) + (offset, reverse, current_position) = self.cursor # TODO: Invalid cursors should 404 - if self.cursor is not None and self.cursor.position != '': - kwargs = {self.ordering + '__gt': self.cursor.position} + # Cursor pagination always enforces an ordering. + if reverse: + queryset = queryset.order_by('-' + self.ordering) + else: + queryset = queryset.order_by(self.ordering) + + # If we have a cursor with a fixed position then filter by that. + if current_position != '': + if self.cursor.reverse: + kwargs = {self.ordering + '__lt': current_position} + else: + kwargs = {self.ordering + '__gt': current_position} queryset = queryset.filter(**kwargs) - # The offset is used in order to deal with cases where we have - # items with an identical position. This allows the cursors - # to gracefully deal with non-unique fields as the ordering. - offset = 0 if (self.cursor is None) else self.cursor.offset - - # We fetch an extra item in order to determine if there is a next page. + # If we have an offset cursor then offset the entire page by that amount. + # We also always fetch an extra item in order to determine if there is a + # page following on from this one. results = list(queryset[offset:offset + self.page_size + 1]) self.page = results[:self.page_size] - self.has_next = len(results) > len(self.page) - self.next_item = results[-1] if self.has_next else None + + # Determine the position of the final item following the page. + if len(results) > len(self.page): + has_following_postion = True + following_position = self._get_position_from_instance(results[-1], self.ordering) + else: + has_following_postion = False + following_position = None + + # If we have a reverse queryset, then the query ordering was in reverse + # so we need to reverse the items again before returning them to the user. + if reverse: + self.page = reversed(self.page) + + if reverse: + # Determine next and previous positions for reverse cursors. + self.has_next = current_position != '' or offset > 0 + self.has_previous = has_following_postion + if self.has_next: + self.next_position = current_position + if self.has_previous: + self.previous_position = following_position + else: + # Determine next and previous positions for forward cursors. + self.has_next = has_following_postion + self.has_previous = current_position != '' or offset > 0 + if self.has_next: + self.next_position = following_position + if self.has_previous: + self.previous_position = current_position + return self.page def get_next_link(self): if not self.has_next: return None - compare = self.get_position_from_instance(self.next_item, self.ordering) + compare = self.next_position offset = 0 for item in reversed(self.page): - position = self.get_position_from_instance(item, self.ordering) + position = self._get_position_from_instance(item, self.ordering) if position != compare: # The item in this position and the item following it # have different positions. We can use this position as @@ -459,26 +498,73 @@ class CursorPagination(BasePagination): offset += 1 else: - if self.cursor is None: - # There were no unique positions in the page, and we were - # on the first page, ie. there was no existing cursor. + # There were no unique positions in the page. + if not self.has_previous: + # We are on the first page. # Our cursor will have an offset equal to the page size, # but no position to filter against yet. offset = self.page_size position = '' + elif self.cursor.reverse: + # The change in direction will introduce a paging artifact, + # where we end up skipping forward a few extra items. + offset = 0 + position = self.previous_position else: - # There were no unique positions in the page. # Use the position from the existing cursor and increment # it's offset by the page size. offset = self.cursor.offset + self.page_size - position = self.cursor.position + position = self.previous_position cursor = Cursor(offset=offset, reverse=False, position=position) encoded = encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) + def get_previous_link(self): + if not self.has_previous: + return None + + compare = self.previous_position + offset = 0 + for item in self.page: + position = self._get_position_from_instance(item, self.ordering) + if position != compare: + # The item in this position and the item following it + # have different positions. We can use this position as + # our marker. + break + + # The item in this postion has the same position as the item + # following it, we can't use it as a marker position, so increment + # the offset and keep seeking to the previous item. + compare = position + offset += 1 + + else: + # There were no unique positions in the page. + if not self.has_next: + # We are on the final page. + # Our cursor will have an offset equal to the page size, + # but no position to filter against yet. + offset = self.page_size + position = '' + elif self.cursor.reverse: + # Use the position from the existing cursor and increment + # it's offset by the page size. + offset = self.cursor.offset + self.page_size + position = self.next_position + else: + # The change in direction will introduce a paging artifact, + # where we end up skipping back a few extra items. + offset = 0 + position = self.next_position + + cursor = Cursor(offset=offset, reverse=True, position=position) + encoded = encode_cursor(cursor) + return replace_query_param(self.base_url, self.cursor_query_param, encoded) + def get_ordering(self): return 'created' - def get_position_from_instance(self, instance, ordering): + def _get_position_from_instance(self, instance, ordering): return str(getattr(instance, ordering)) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index f04079a72..47019671f 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -442,6 +442,9 @@ class TestCursorPagination: if item.created > int(created__gt) ] + def order_by(self, ordering): + return self + def __getitem__(self, sliced): return self.items[sliced] @@ -503,6 +506,9 @@ class TestCrazyCursorPagination: if item.created > int(created__gt) ] + def order_by(self, ordering): + return self + def __getitem__(self, sliced): return self.items[sliced] From f1af603fb05fce236a4258e18df8af8888043247 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 10:51:04 +0000 Subject: [PATCH 083/301] Tests for reverse pagination --- rest_framework/pagination.py | 2 + tests/test_pagination.py | 98 ++++++++++++++++++++++++++---------- 2 files changed, 73 insertions(+), 27 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 9e22a8bf3..d5af2ac81 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -408,6 +408,8 @@ def encode_cursor(cursor): class CursorPagination(BasePagination): # TODO: handle queries with '' as a legitimate position + # Support case where ordering is already negative + # Support tuple orderings cursor_query_param = 'cursor' page_size = 5 diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 47019671f..4907a0807 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -436,13 +436,22 @@ class TestCursorPagination: def __init__(self, items): self.items = items - def filter(self, created__gt): - return [ + def filter(self, created__gt=None, created__lt=None): + if created__gt is not None: + return MockQuerySet([ + item for item in self.items + if item.created > int(created__gt) + ]) + + assert created__lt is not None + return MockQuerySet([ item for item in self.items - if item.created > int(created__gt) - ] + if item.created < int(created__lt) + ]) def order_by(self, ordering): + if ordering.startswith('-'): + return MockQuerySet(reversed(self.items)) return self def __getitem__(self, sliced): @@ -485,6 +494,25 @@ class TestCursorPagination: next_url = self.pagination.get_next_link() assert next_url is None + # Now page back again + + previous_url = self.pagination.get_previous_link() + assert previous_url + + request = Request(factory.get(previous_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [6, 7, 8, 9, 10] + + previous_url = self.pagination.get_previous_link() + assert previous_url + + request = Request(factory.get(previous_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 2, 3, 4, 5] + + previous_url = self.pagination.get_previous_link() + assert previous_url is None + class TestCrazyCursorPagination: """ @@ -500,13 +528,22 @@ class TestCrazyCursorPagination: def __init__(self, items): self.items = items - def filter(self, created__gt): - return [ + def filter(self, created__gt=None, created__lt=None): + if created__gt is not None: + return MockQuerySet([ + item for item in self.items + if item.created > int(created__gt) + ]) + + assert created__lt is not None + return MockQuerySet([ item for item in self.items - if item.created > int(created__gt) - ] + if item.created < int(created__lt) + ]) def order_by(self, ordering): + if ordering.startswith('-'): + return MockQuerySet(reversed(self.items)) return self def __getitem__(self, sliced): @@ -553,25 +590,32 @@ class TestCrazyCursorPagination: next_url = self.pagination.get_next_link() assert next_url is None - # assert content == { - # 'results': [1, 2, 3, 4, 5], - # 'previous': None, - # 'next': 'http://testserver/?limit=5&offset=5', - # 'count': 100 - # } - # assert context == { - # 'previous_url': None, - # 'next_url': 'http://testserver/?limit=5&offset=5', - # 'page_links': [ - # PageLink('http://testserver/?limit=5', 1, True, False), - # PageLink('http://testserver/?limit=5&offset=5', 2, False, False), - # PageLink('http://testserver/?limit=5&offset=10', 3, False, False), - # PAGE_BREAK, - # PageLink('http://testserver/?limit=5&offset=95', 20, False, False), - # ] - # } - # assert self.pagination.display_page_controls - # assert isinstance(self.pagination.to_html(), type('')) + + # Now page back again + + previous_url = self.pagination.get_previous_link() + assert previous_url + + request = Request(factory.get(previous_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 1, 2, 3, 4] + + previous_url = self.pagination.get_previous_link() + assert previous_url + + request = Request(factory.get(previous_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + + previous_url = self.pagination.get_previous_link() + assert previous_url + + request = Request(factory.get(previous_url)) + queryset = self.paginate_queryset(request) + assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + + previous_url = self.pagination.get_previous_link() + assert previous_url is None def test_get_displayed_page_numbers(): From 94b5f7a86e401e46f14fb8982afaa7a8c61847c9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 12:14:52 +0000 Subject: [PATCH 084/301] Tidy up cursor tests and make more comprehensive --- rest_framework/pagination.py | 30 ++++- tests/test_pagination.py | 242 ++++++++++++++--------------------- 2 files changed, 122 insertions(+), 150 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index d5af2ac81..618352391 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -171,6 +171,8 @@ class PageNumberPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' + invalid_page_message = _('Invalid page "{page_number}": {message}.') + def _handle_backwards_compat(self, view): """ Prior to version 3.1, pagination was handled in the view, and the @@ -203,7 +205,7 @@ class PageNumberPagination(BasePagination): try: self.page = paginator.page(page_number) except InvalidPage as exc: - msg = _('Invalid page "{page_number}": {message}.').format( + msg = self.invalid_page_message.format( page_number=page_number, message=six.text_type(exc) ) raise NotFound(msg) @@ -386,8 +388,8 @@ Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) def decode_cursor(encoded): - tokens = urlparse.parse_qs(b64decode(encoded), keep_blank_values=True) try: + tokens = urlparse.parse_qs(b64decode(encoded), keep_blank_values=True) offset = int(tokens['offset'][0]) reverse = bool(int(tokens['reverse'][0])) position = tokens['position'][0] @@ -411,7 +413,8 @@ class CursorPagination(BasePagination): # Support case where ordering is already negative # Support tuple orderings cursor_query_param = 'cursor' - page_size = 5 + page_size = api_settings.PAGINATE_BY + invalid_cursor_message = _('Invalid cursor') def paginate_queryset(self, queryset, request, view=None): self.base_url = request.build_absolute_uri() @@ -424,8 +427,9 @@ class CursorPagination(BasePagination): (offset, reverse, current_position) = (0, False, '') else: self.cursor = decode_cursor(encoded) + if self.cursor is None: + raise NotFound(self.invalid_cursor_message) (offset, reverse, current_position) = self.cursor - # TODO: Invalid cursors should 404 # Cursor pagination always enforces an ordering. if reverse: @@ -458,7 +462,7 @@ class CursorPagination(BasePagination): # If we have a reverse queryset, then the query ordering was in reverse # so we need to reverse the items again before returning them to the user. if reverse: - self.page = reversed(self.page) + self.page = list(reversed(self.page)) if reverse: # Determine next and previous positions for reverse cursors. @@ -483,8 +487,14 @@ class CursorPagination(BasePagination): if not self.has_next: return None - compare = self.next_position + if self.cursor and self.cursor.reverse and self.cursor.offset != 0: + # If we're reversing direction and we have an offset cursor + # then we cannot use the first position we find as a marker. + compare = self._get_position_from_instance(self.page[-1], self.ordering) + else: + compare = self.next_position offset = 0 + for item in reversed(self.page): position = self._get_position_from_instance(item, self.ordering) if position != compare: @@ -526,8 +536,14 @@ class CursorPagination(BasePagination): if not self.has_previous: return None - compare = self.previous_position + if self.cursor and not self.cursor.reverse and self.cursor.offset != 0: + # If we're reversing direction and we have an offset cursor + # then we cannot use the first position we find as a marker. + compare = self._get_position_from_instance(self.page[0], self.ordering) + else: + compare = self.previous_position offset = 0 + for item in self.page: position = self._get_position_from_instance(item, self.ordering) if position != compare: diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 4907a0807..e32dd0288 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -451,171 +451,127 @@ class TestCursorPagination: def order_by(self, ordering): if ordering.startswith('-'): - return MockQuerySet(reversed(self.items)) + return MockQuerySet(list(reversed(self.items))) return self def __getitem__(self, sliced): return self.items[sliced] - self.pagination = pagination.CursorPagination() - self.queryset = MockQuerySet( - [MockObject(idx) for idx in range(1, 16)] - ) + class ExamplePagination(pagination.CursorPagination): + page_size = 5 - def paginate_queryset(self, request): - return list(self.pagination.paginate_queryset(self.queryset, request)) - - # def get_paginated_content(self, queryset): - # response = self.pagination.get_paginated_response(queryset) - # return response.data - - # def get_html_context(self): - # return self.pagination.get_html_context() - - def test_following_cursor(self): - request = Request(factory.get('/')) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 2, 3, 4, 5] - - next_url = self.pagination.get_next_link() - assert next_url - - request = Request(factory.get(next_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [6, 7, 8, 9, 10] - - next_url = self.pagination.get_next_link() - assert next_url - - request = Request(factory.get(next_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [11, 12, 13, 14, 15] - - next_url = self.pagination.get_next_link() - assert next_url is None - - # Now page back again - - previous_url = self.pagination.get_previous_link() - assert previous_url - - request = Request(factory.get(previous_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [6, 7, 8, 9, 10] - - previous_url = self.pagination.get_previous_link() - assert previous_url - - request = Request(factory.get(previous_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 2, 3, 4, 5] - - previous_url = self.pagination.get_previous_link() - assert previous_url is None - - -class TestCrazyCursorPagination: - """ - Unit tests for `pagination.CursorPagination`. - """ - - def setup(self): - class MockObject(object): - def __init__(self, idx): - self.created = idx - - class MockQuerySet(object): - def __init__(self, items): - self.items = items - - def filter(self, created__gt=None, created__lt=None): - if created__gt is not None: - return MockQuerySet([ - item for item in self.items - if item.created > int(created__gt) - ]) - - assert created__lt is not None - return MockQuerySet([ - item for item in self.items - if item.created < int(created__lt) - ]) - - def order_by(self, ordering): - if ordering.startswith('-'): - return MockQuerySet(reversed(self.items)) - return self - - def __getitem__(self, sliced): - return self.items[sliced] - - self.pagination = pagination.CursorPagination() + self.pagination = ExamplePagination() self.queryset = MockQuerySet([ MockObject(idx) for idx in [ 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, - 1, 1, 2, 3, 4, - 5, 6, 7, 8, 9 + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + 7, 7, 7, 7, 7, + 7, 7, 7, 8, 9, + 9, 9, 9, 9, 9 ] ]) - def paginate_queryset(self, request): - return list(self.pagination.paginate_queryset(self.queryset, request)) + def get_pages(self, url): + """ + Given a URL return a tuple of: - def test_following_cursor_identical_items(self): - request = Request(factory.get('/')) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + (previous page, current page, next page, previous url, next url) + """ + request = Request(factory.get(url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + current = [item.created for item in queryset] next_url = self.pagination.get_next_link() - assert next_url - - request = Request(factory.get(next_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 1, 1, 1] - - next_url = self.pagination.get_next_link() - assert next_url - - request = Request(factory.get(next_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 2, 3, 4] - - next_url = self.pagination.get_next_link() - assert next_url - - request = Request(factory.get(next_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [5, 6, 7, 8, 9] - - next_url = self.pagination.get_next_link() - assert next_url is None - - # Now page back again - previous_url = self.pagination.get_previous_link() - assert previous_url - request = Request(factory.get(previous_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 2, 3, 4] + if next_url is not None: + request = Request(factory.get(next_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + next = [item.created for item in queryset] + else: + next = None - previous_url = self.pagination.get_previous_link() - assert previous_url + if previous_url is not None: + request = Request(factory.get(previous_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + previous = [item.created for item in queryset] + else: + previous = None - request = Request(factory.get(previous_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + return (previous, current, next, previous_url, next_url) - previous_url = self.pagination.get_previous_link() - assert previous_url + def test_invalid_cursor(self): + request = Request(factory.get('/', {'cursor': '123'})) + with pytest.raises(exceptions.NotFound): + self.pagination.paginate_queryset(self.queryset, request) - request = Request(factory.get(previous_url)) - queryset = self.paginate_queryset(request) - assert [item.created for item in queryset] == [1, 1, 1, 1, 1] + def test_cursor_pagination(self): + (previous, current, next, previous_url, next_url) = self.get_pages('/') - previous_url = self.pagination.get_previous_link() - assert previous_url is None + assert previous is None + assert current == [1, 1, 1, 1, 1] + assert next == [1, 2, 3, 4, 4] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [1, 1, 1, 1, 1] + assert current == [1, 2, 3, 4, 4] + assert next == [4, 4, 5, 6, 7] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [1, 2, 3, 4, 4] + assert current == [4, 4, 5, 6, 7] + assert next == [7, 7, 7, 7, 7] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [4, 4, 4, 5, 6] # Paging artifact + assert current == [7, 7, 7, 7, 7] + assert next == [7, 7, 7, 8, 9] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [7, 7, 7, 7, 7] + assert current == [7, 7, 7, 8, 9] + assert next == [9, 9, 9, 9, 9] + + (previous, current, next, previous_url, next_url) = self.get_pages(next_url) + + assert previous == [7, 7, 7, 8, 9] + assert current == [9, 9, 9, 9, 9] + assert next is None + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [7, 7, 7, 7, 7] + assert current == [7, 7, 7, 8, 9] + assert next == [9, 9, 9, 9, 9] + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [4, 4, 5, 6, 7] + assert current == [7, 7, 7, 7, 7] + assert next == [8, 9, 9, 9, 9] # Paging artifact + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [1, 2, 3, 4, 4] + assert current == [4, 4, 5, 6, 7] + assert next == [7, 7, 7, 7, 7] + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous == [1, 1, 1, 1, 1] + assert current == [1, 2, 3, 4, 4] + assert next == [4, 4, 5, 6, 7] + + (previous, current, next, previous_url, next_url) = self.get_pages(previous_url) + + assert previous is None + assert current == [1, 1, 1, 1, 1] + assert next == [1, 2, 3, 4, 4] def test_get_displayed_page_numbers(): From ca372ef6ef1cf95eb9282a484782e1a3721cb72b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 13:50:51 +0000 Subject: [PATCH 085/301] Fix for python 3 --- rest_framework/pagination.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 618352391..0c5abccb7 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -389,7 +389,7 @@ Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) def decode_cursor(encoded): try: - tokens = urlparse.parse_qs(b64decode(encoded), keep_blank_values=True) + tokens = urlparse.parse_qs(b64decode(encoded).decode('ascii'), keep_blank_values=True) offset = int(tokens['offset'][0]) reverse = bool(int(tokens['reverse'][0])) position = tokens['position'][0] @@ -405,7 +405,7 @@ def encode_cursor(cursor): 'reverse': '1' if cursor.reverse else '0', 'position': cursor.position } - return b64encode(urlparse.urlencode(tokens, doseq=True)) + return b64encode(urlparse.urlencode(tokens, doseq=True).encode('ascii')) class CursorPagination(BasePagination): From 38a2ed6f62adcdcb2eba94f6133d4dd976a53af1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 14:04:25 +0000 Subject: [PATCH 086/301] Python 3 fixes for cursor pagination --- rest_framework/pagination.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 0c5abccb7..cf1f1afa1 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -389,7 +389,8 @@ Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) def decode_cursor(encoded): try: - tokens = urlparse.parse_qs(b64decode(encoded).decode('ascii'), keep_blank_values=True) + querystring = b64decode(encoded.encode('ascii')).decode('ascii') + tokens = urlparse.parse_qs(querystring, keep_blank_values=True) offset = int(tokens['offset'][0]) reverse = bool(int(tokens['reverse'][0])) position = tokens['position'][0] @@ -405,7 +406,8 @@ def encode_cursor(cursor): 'reverse': '1' if cursor.reverse else '0', 'position': cursor.position } - return b64encode(urlparse.urlencode(tokens, doseq=True).encode('ascii')) + querystring = urlparse.urlencode(tokens, doseq=True) + return b64encode(querystring.encode('ascii')).decode('ascii') class CursorPagination(BasePagination): From 83a82b44a56a303d43a16dd675fae116e51b9d85 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 15:07:01 +0000 Subject: [PATCH 087/301] Support for tuple ordering in cursor pagination --- rest_framework/pagination.py | 113 +++++++++++++++++++++-------------- tests/test_pagination.py | 2 +- 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index cf1f1afa1..58223f49b 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -20,12 +20,12 @@ from rest_framework.utils.urls import ( ) -def _strict_positive_int(integer_string, cutoff=None): +def _positive_int(integer_string, strict=False, cutoff=None): """ Cast a string to a strictly positive integer. """ ret = int(integer_string) - if ret <= 0: + if ret < 0 or (ret == 0 and strict): raise ValueError() if cutoff: ret = min(ret, cutoff) @@ -126,6 +126,47 @@ def _get_page_links(page_numbers, current, url_func): return page_links +def _decode_cursor(encoded): + """ + Given a string representing an encoded cursor, return a `Cursor` instance. + """ + try: + querystring = b64decode(encoded.encode('ascii')).decode('ascii') + tokens = urlparse.parse_qs(querystring, keep_blank_values=True) + offset = _positive_int(tokens['offset'][0]) + reverse = bool(int(tokens['reverse'][0])) + position = tokens.get('position', [None])[0] + except (TypeError, ValueError): + return None + + return Cursor(offset=offset, reverse=reverse, position=position) + + +def _encode_cursor(cursor): + """ + Given a Cursor instance, return an encoded string representation. + """ + tokens = { + 'offset': str(cursor.offset), + 'reverse': '1' if cursor.reverse else '0', + } + if cursor.position is not None: + tokens['position'] = cursor.position + + querystring = urlparse.urlencode(tokens, doseq=True) + return b64encode(querystring.encode('ascii')).decode('ascii') + + +def _reverse_ordering(ordering_tuple): + """ + Given an order_by tuple such as `('-created', 'uuid')` reverse the + ordering and return a new tuple, eg. `('created', '-uuid')`. + """ + invert = lambda x: x[1:] if (x.startswith('-')) else '-' + x + return tuple([invert(item) for item in ordering_tuple]) + + +Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break']) PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True) @@ -228,8 +269,9 @@ class PageNumberPagination(BasePagination): def get_page_size(self, request): if self.paginate_by_param: try: - return _strict_positive_int( + return _positive_int( request.query_params[self.paginate_by_param], + strict=True, cutoff=self.max_paginate_by ) except (KeyError, ValueError): @@ -312,7 +354,7 @@ class LimitOffsetPagination(BasePagination): def get_limit(self, request): if self.limit_query_param: try: - return _strict_positive_int( + return _positive_int( request.query_params[self.limit_query_param], cutoff=self.max_limit ) @@ -323,7 +365,7 @@ class LimitOffsetPagination(BasePagination): def get_offset(self, request): try: - return _strict_positive_int( + return _positive_int( request.query_params[self.offset_query_param], ) except (KeyError, ValueError): @@ -384,36 +426,10 @@ class LimitOffsetPagination(BasePagination): return template.render(context) -Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position']) - - -def decode_cursor(encoded): - try: - querystring = b64decode(encoded.encode('ascii')).decode('ascii') - tokens = urlparse.parse_qs(querystring, keep_blank_values=True) - offset = int(tokens['offset'][0]) - reverse = bool(int(tokens['reverse'][0])) - position = tokens['position'][0] - except (TypeError, ValueError): - return None - - return Cursor(offset=offset, reverse=reverse, position=position) - - -def encode_cursor(cursor): - tokens = { - 'offset': str(cursor.offset), - 'reverse': '1' if cursor.reverse else '0', - 'position': cursor.position - } - querystring = urlparse.urlencode(tokens, doseq=True) - return b64encode(querystring.encode('ascii')).decode('ascii') - - class CursorPagination(BasePagination): - # TODO: handle queries with '' as a legitimate position # Support case where ordering is already negative # Support tuple orderings + # Determine how/if True, False and None positions work cursor_query_param = 'cursor' page_size = api_settings.PAGINATE_BY invalid_cursor_message = _('Invalid cursor') @@ -426,25 +442,26 @@ class CursorPagination(BasePagination): encoded = request.query_params.get(self.cursor_query_param) if encoded is None: self.cursor = None - (offset, reverse, current_position) = (0, False, '') + (offset, reverse, current_position) = (0, False, None) else: - self.cursor = decode_cursor(encoded) + self.cursor = _decode_cursor(encoded) if self.cursor is None: raise NotFound(self.invalid_cursor_message) (offset, reverse, current_position) = self.cursor # Cursor pagination always enforces an ordering. if reverse: - queryset = queryset.order_by('-' + self.ordering) + queryset = queryset.order_by(_reverse_ordering(self.ordering)) else: queryset = queryset.order_by(self.ordering) # If we have a cursor with a fixed position then filter by that. - if current_position != '': + if current_position is not None: + primary_ordering_attr = self.ordering[0].lstrip('-') if self.cursor.reverse: - kwargs = {self.ordering + '__lt': current_position} + kwargs = {primary_ordering_attr + '__lt': current_position} else: - kwargs = {self.ordering + '__gt': current_position} + kwargs = {primary_ordering_attr + '__gt': current_position} queryset = queryset.filter(**kwargs) # If we have an offset cursor then offset the entire page by that amount. @@ -468,7 +485,7 @@ class CursorPagination(BasePagination): if reverse: # Determine next and previous positions for reverse cursors. - self.has_next = current_position != '' or offset > 0 + self.has_next = (current_position is not None) or (offset > 0) self.has_previous = has_following_postion if self.has_next: self.next_position = current_position @@ -477,7 +494,7 @@ class CursorPagination(BasePagination): else: # Determine next and previous positions for forward cursors. self.has_next = has_following_postion - self.has_previous = current_position != '' or offset > 0 + self.has_previous = (current_position is not None) or (offset > 0) if self.has_next: self.next_position = following_position if self.has_previous: @@ -518,7 +535,7 @@ class CursorPagination(BasePagination): # Our cursor will have an offset equal to the page size, # but no position to filter against yet. offset = self.page_size - position = '' + position = None elif self.cursor.reverse: # The change in direction will introduce a paging artifact, # where we end up skipping forward a few extra items. @@ -531,7 +548,7 @@ class CursorPagination(BasePagination): position = self.previous_position cursor = Cursor(offset=offset, reverse=False, position=position) - encoded = encode_cursor(cursor) + encoded = _encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) def get_previous_link(self): @@ -567,7 +584,7 @@ class CursorPagination(BasePagination): # Our cursor will have an offset equal to the page size, # but no position to filter against yet. offset = self.page_size - position = '' + position = None elif self.cursor.reverse: # Use the position from the existing cursor and increment # it's offset by the page size. @@ -580,11 +597,15 @@ class CursorPagination(BasePagination): position = self.next_position cursor = Cursor(offset=offset, reverse=True, position=position) - encoded = encode_cursor(cursor) + encoded = _encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) def get_ordering(self): - return 'created' + """ + Return a tuple of strings, that may be used in an `order_by` method. + """ + return ('created',) def _get_position_from_instance(self, instance, ordering): - return str(getattr(instance, ordering)) + attr = getattr(instance, ordering[0]) + return six.text_type(attr) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index e32dd0288..fffdcbe92 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -450,7 +450,7 @@ class TestCursorPagination: ]) def order_by(self, ordering): - if ordering.startswith('-'): + if ordering[0].startswith('-'): return MockQuerySet(list(reversed(self.items))) return self From 408261ee02b176732b7f840f7042e7c24f3ecd27 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 15:15:52 +0000 Subject: [PATCH 088/301] Support ordering attribute either on view or on pagination class for CursorPagination --- rest_framework/pagination.py | 24 +++++++++++++++++++----- tests/test_pagination.py | 1 + 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 58223f49b..7b28b47f0 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -427,16 +427,16 @@ class LimitOffsetPagination(BasePagination): class CursorPagination(BasePagination): - # Support case where ordering is already negative - # Support tuple orderings + # Support usage with OrderingFilter # Determine how/if True, False and None positions work cursor_query_param = 'cursor' page_size = api_settings.PAGINATE_BY invalid_cursor_message = _('Invalid cursor') + ordering = None def paginate_queryset(self, queryset, request, view=None): self.base_url = request.build_absolute_uri() - self.ordering = self.get_ordering() + self.ordering = self.get_ordering(view) # Determine if we have a cursor, and if so then decode it. encoded = request.query_params.get(self.cursor_query_param) @@ -600,11 +600,25 @@ class CursorPagination(BasePagination): encoded = _encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) - def get_ordering(self): + def get_ordering(self, view): """ Return a tuple of strings, that may be used in an `order_by` method. """ - return ('created',) + ordering = getattr(view, 'ordering', getattr(self, 'ordering', None)) + + assert ordering is not None, ( + 'Using cursor pagination, but no ordering attribute was declared ' + 'on the view or on the pagination class.' + ) + assert isinstance(ordering, (six.string_types, list, tuple)), ( + 'Invalid ordering. Expected string or tuple, but got {type}'.format( + type=type(ordering).__name__ + ) + ) + + if isinstance(ordering, six.string_types): + return (ordering,) + return ordering def _get_position_from_instance(self, instance, ordering): attr = getattr(instance, ordering[0]) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index fffdcbe92..c05b4abab 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -459,6 +459,7 @@ class TestCursorPagination: class ExamplePagination(pagination.CursorPagination): page_size = 5 + ordering = 'created' self.pagination = ExamplePagination() self.queryset = MockQuerySet([ From 0822c9e55820f8e4737329e38abc2e21718af9e5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 16:12:05 +0000 Subject: [PATCH 089/301] Cursor pagination now works with OrderingFilter --- rest_framework/filters.py | 24 ++++++++++----------- rest_framework/pagination.py | 41 +++++++++++++++++++++++++++--------- tests/test_pagination.py | 40 +++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 23 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index d188a2d1e..2bcf36991 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -114,7 +114,7 @@ class OrderingFilter(BaseFilterBackend): ordering_param = api_settings.ORDERING_PARAM ordering_fields = None - def get_ordering(self, request): + def get_ordering(self, request, queryset, view): """ Ordering is set by a comma delimited ?ordering=... query parameter. @@ -124,7 +124,13 @@ class OrderingFilter(BaseFilterBackend): """ params = request.query_params.get(self.ordering_param) if params: - return [param.strip() for param in params.split(',')] + fields = [param.strip() for param in params.split(',')] + ordering = self.remove_invalid_fields(queryset, fields, view) + if ordering: + return ordering + + # No ordering was included, or all the ordering fields were invalid + return self.get_default_ordering(view) def get_default_ordering(self, view): ordering = getattr(view, 'ordering', None) @@ -132,7 +138,7 @@ class OrderingFilter(BaseFilterBackend): return (ordering,) return ordering - def remove_invalid_fields(self, queryset, ordering, view): + def remove_invalid_fields(self, queryset, fields, view): valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) if valid_fields is None: @@ -152,18 +158,10 @@ class OrderingFilter(BaseFilterBackend): valid_fields = [field.name for field in queryset.model._meta.fields] valid_fields += queryset.query.aggregates.keys() - return [term for term in ordering if term.lstrip('-') in valid_fields] + return [term for term in fields if term.lstrip('-') in valid_fields] def filter_queryset(self, request, queryset, view): - ordering = self.get_ordering(request) - - if ordering: - # Skip any incorrect parameters - ordering = self.remove_invalid_fields(queryset, ordering, view) - - if not ordering: - # Use 'ordering' attribute by default - ordering = self.get_default_ordering(view) + ordering = self.get_ordering(request, queryset, view) if ordering: return queryset.order_by(*ordering) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 7b28b47f0..1b4174bc6 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -427,8 +427,9 @@ class LimitOffsetPagination(BasePagination): class CursorPagination(BasePagination): - # Support usage with OrderingFilter - # Determine how/if True, False and None positions work + # Determine how/if True, False and None positions work - do the string + # encodings work with Django queryset filters? + # Consider a max offset cap. cursor_query_param = 'cursor' page_size = api_settings.PAGINATE_BY invalid_cursor_message = _('Invalid cursor') @@ -436,7 +437,7 @@ class CursorPagination(BasePagination): def paginate_queryset(self, queryset, request, view=None): self.base_url = request.build_absolute_uri() - self.ordering = self.get_ordering(view) + self.ordering = self.get_ordering(request, queryset, view) # Determine if we have a cursor, and if so then decode it. encoded = request.query_params.get(self.cursor_query_param) @@ -600,16 +601,36 @@ class CursorPagination(BasePagination): encoded = _encode_cursor(cursor) return replace_query_param(self.base_url, self.cursor_query_param, encoded) - def get_ordering(self, view): + def get_ordering(self, request, queryset, view): """ Return a tuple of strings, that may be used in an `order_by` method. """ - ordering = getattr(view, 'ordering', getattr(self, 'ordering', None)) + ordering_filters = [ + filter_cls for filter_cls in getattr(view, 'filter_backends', []) + if hasattr(filter_cls, 'get_ordering') + ] + + if ordering_filters: + # If a filter exists on the view that implements `get_ordering` + # then we defer to that filter to determine the ordering. + filter_cls = ordering_filters[0] + filter_instance = filter_cls() + ordering = filter_instance.get_ordering(request, queryset, view) + assert ordering is not None, ( + 'Using cursor pagination, but filter class {filter_cls} ' + 'returned a `None` ordering.'.format( + filter_cls=filter_cls.__name__ + ) + ) + else: + # The default case is to check for an `ordering` attribute, + # first on the view instance, and then on this pagination instance. + ordering = getattr(view, 'ordering', getattr(self, 'ordering', None)) + assert ordering is not None, ( + 'Using cursor pagination, but no ordering attribute was declared ' + 'on the view or on the pagination class.' + ) - assert ordering is not None, ( - 'Using cursor pagination, but no ordering attribute was declared ' - 'on the view or on the pagination class.' - ) assert isinstance(ordering, (six.string_types, list, tuple)), ( 'Invalid ordering. Expected string or tuple, but got {type}'.format( type=type(ordering).__name__ @@ -618,7 +639,7 @@ class CursorPagination(BasePagination): if isinstance(ordering, six.string_types): return (ordering,) - return ordering + return tuple(ordering) def _get_position_from_instance(self, instance, ordering): attr = getattr(instance, ordering[0]) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index c05b4abab..338be610c 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -77,6 +77,20 @@ class TestPaginationIntegration: 'count': 50 } + def test_setting_page_size_to_zero(self): + """ + When page_size parameter is invalid it should return to the default. + """ + request = factory.get('/', {'page_size': 0}) + response = self.view(request) + assert response.status_code == status.HTTP_200_OK + assert response.data == { + 'results': [2, 4, 6, 8, 10], + 'previous': None, + 'next': 'http://testserver/?page=2&page_size=0', + 'count': 50 + } + def test_additional_query_params_are_preserved(self): request = factory.get('/', {'page': 2, 'filter': 'even'}) response = self.view(request) @@ -88,6 +102,14 @@ class TestPaginationIntegration: 'count': 50 } + def test_404_not_found_for_zero_page(self): + request = factory.get('/', {'page': '0'}) + response = self.view(request) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == { + 'detail': 'Invalid page "0": That page number is less than 1.' + } + def test_404_not_found_for_invalid_page(self): request = factory.get('/', {'page': 'invalid'}) response = self.view(request) @@ -507,6 +529,24 @@ class TestCursorPagination: with pytest.raises(exceptions.NotFound): self.pagination.paginate_queryset(self.queryset, request) + def test_use_with_ordering_filter(self): + class MockView: + filter_backends = (filters.OrderingFilter,) + ordering_fields = ['username', 'created'] + ordering = 'created' + + request = Request(factory.get('/', {'ordering': 'username'})) + ordering = self.pagination.get_ordering(request, [], MockView()) + assert ordering == ('username',) + + request = Request(factory.get('/', {'ordering': '-username'})) + ordering = self.pagination.get_ordering(request, [], MockView()) + assert ordering == ('-username',) + + request = Request(factory.get('/', {'ordering': 'invalid'})) + ordering = self.pagination.get_ordering(request, [], MockView()) + assert ordering == ('created',) + def test_cursor_pagination(self): (previous, current, next, previous_url, next_url) = self.get_pages('/') From 43d983fae82ab23ca94f52deb29e938eb2a40e88 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 22 Jan 2015 17:25:12 +0000 Subject: [PATCH 090/301] Add paging controls --- rest_framework/pagination.py | 66 ++++++++++++++----- .../rest_framework/css/bootstrap-tweaks.css | 12 +++- .../pagination/previous_and_next.html | 12 ++++ tests/test_pagination.py | 5 +- 4 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 rest_framework/templates/rest_framework/pagination/previous_and_next.html diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1b4174bc6..b3658acad 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -133,9 +133,14 @@ def _decode_cursor(encoded): try: querystring = b64decode(encoded.encode('ascii')).decode('ascii') tokens = urlparse.parse_qs(querystring, keep_blank_values=True) - offset = _positive_int(tokens['offset'][0]) - reverse = bool(int(tokens['reverse'][0])) - position = tokens.get('position', [None])[0] + + offset = tokens.get('o', ['0'])[0] + offset = _positive_int(offset) + + reverse = tokens.get('r', ['0'])[0] + reverse = bool(int(reverse)) + + position = tokens.get('p', [None])[0] except (TypeError, ValueError): return None @@ -146,12 +151,13 @@ def _encode_cursor(cursor): """ Given a Cursor instance, return an encoded string representation. """ - tokens = { - 'offset': str(cursor.offset), - 'reverse': '1' if cursor.reverse else '0', - } + tokens = {} + if cursor.offset != 0: + tokens['o'] = str(cursor.offset) + if cursor.reverse: + tokens['r'] = '1' if cursor.position is not None: - tokens['position'] = cursor.position + tokens['p'] = cursor.position querystring = urlparse.urlencode(tokens, doseq=True) return b64encode(querystring.encode('ascii')).decode('ascii') @@ -430,10 +436,12 @@ class CursorPagination(BasePagination): # Determine how/if True, False and None positions work - do the string # encodings work with Django queryset filters? # Consider a max offset cap. + # Tidy up the `get_ordering` API (eg remove queryset from it) cursor_query_param = 'cursor' page_size = api_settings.PAGINATE_BY invalid_cursor_message = _('Invalid cursor') ordering = None + template = 'rest_framework/pagination/previous_and_next.html' def paginate_queryset(self, queryset, request, view=None): self.base_url = request.build_absolute_uri() @@ -452,17 +460,22 @@ class CursorPagination(BasePagination): # Cursor pagination always enforces an ordering. if reverse: - queryset = queryset.order_by(_reverse_ordering(self.ordering)) + queryset = queryset.order_by(*_reverse_ordering(self.ordering)) else: - queryset = queryset.order_by(self.ordering) + queryset = queryset.order_by(*self.ordering) # If we have a cursor with a fixed position then filter by that. if current_position is not None: - primary_ordering_attr = self.ordering[0].lstrip('-') - if self.cursor.reverse: - kwargs = {primary_ordering_attr + '__lt': current_position} + order = self.ordering[0] + is_reversed = order.startswith('-') + order_attr = order.lstrip('-') + + # Test for: (cursor reversed) XOR (queryset reversed) + if self.cursor.reverse != is_reversed: + kwargs = {order_attr + '__lt': current_position} else: - kwargs = {primary_ordering_attr + '__gt': current_position} + kwargs = {order_attr + '__gt': current_position} + queryset = queryset.filter(**kwargs) # If we have an offset cursor then offset the entire page by that amount. @@ -501,6 +514,11 @@ class CursorPagination(BasePagination): if self.has_previous: self.previous_position = current_position + # Display page controls in the browsable API if there is more + # than one page. + if self.has_previous or self.has_next: + self.display_page_controls = True + return self.page def get_next_link(self): @@ -642,5 +660,23 @@ class CursorPagination(BasePagination): return tuple(ordering) def _get_position_from_instance(self, instance, ordering): - attr = getattr(instance, ordering[0]) + attr = getattr(instance, ordering[0].lstrip('-')) return six.text_type(attr) + + def get_paginated_response(self, data): + return Response(OrderedDict([ + ('next', self.get_next_link()), + ('previous', self.get_previous_link()), + ('results', data) + ])) + + def get_html_context(self): + return { + 'previous_url': self.get_previous_link(), + 'next_url': self.get_next_link() + } + + def to_html(self): + template = loader.get_template(self.template) + context = Context(self.get_html_context()) + return template.render(context) diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index 15b42178f..04f12ed3d 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -63,10 +63,20 @@ a single block in the template. .pagination>.disabled>a, .pagination>.disabled>a:hover, .pagination>.disabled>a:focus { - cursor: default; + cursor: not-allowed; pointer-events: none; } +.pager>.disabled>a, +.pager>.disabled>a:hover, +.pager>.disabled>a:focus { + pointer-events: none; +} + +.pager .next { + margin-left: 10px; +} + /*=== dabapps bootstrap styles ====*/ html { diff --git a/rest_framework/templates/rest_framework/pagination/previous_and_next.html b/rest_framework/templates/rest_framework/pagination/previous_and_next.html new file mode 100644 index 000000000..eacbfff45 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/previous_and_next.html @@ -0,0 +1,12 @@ + diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 338be610c..13bfb6272 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -1,3 +1,4 @@ +# coding: utf-8 from __future__ import unicode_literals from rest_framework import exceptions, generics, pagination, serializers, status, filters from rest_framework.request import Request @@ -471,7 +472,7 @@ class TestCursorPagination: if item.created < int(created__lt) ]) - def order_by(self, ordering): + def order_by(self, *ordering): if ordering[0].startswith('-'): return MockQuerySet(list(reversed(self.items))) return self @@ -614,6 +615,8 @@ class TestCursorPagination: assert current == [1, 1, 1, 1, 1] assert next == [1, 2, 3, 4, 4] + assert isinstance(self.pagination.to_html(), type('')) + def test_get_displayed_page_numbers(): """ From 25a703b42c030f712734ed56b8f1996f8d13ac0c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:15:11 +0000 Subject: [PATCH 091/301] Work around meta API differences --- rest_framework/utils/model_meta.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 375d2e8c6..6a5835f54 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -121,12 +121,17 @@ def _get_reverse_relationships(opts): """ Returns an `OrderedDict` of field names to `RelationInfo`. """ + # Note that we have a hack here to handle internal API differences for + # this internal API across Django 1.7 -> Django 1.8. + # See: https://code.djangoproject.com/ticket/24208 + reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -134,9 +139,10 @@ def _get_reverse_relationships(opts): # Deal with reverse many-to-many relationships. for relation in opts.get_all_related_many_to_many_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) From 5eb6949e9f24dd5e94aa5eb50fd6ccaf34b21878 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:32:04 +0000 Subject: [PATCH 092/301] Drop django-filter from 1.8 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e43a9234f..75ebe1345 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps = {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 pytest-django==2.8.0 - django-filter==0.7 + {py27,py32,py32,py33,py34}-django{14,15,16,17}: django-filter==0.7 defusedxml==0.3 markdown>=2.1.0 PyYAML>=3.10 From e307fd289c52a9bb97a567cff314a479bbdd21df Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:38:58 +0000 Subject: [PATCH 093/301] Fix test matrix --- env/pip-selfcheck.json | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json new file mode 100644 index 000000000..50cde9566 --- /dev/null +++ b/env/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2015-01-23T11:37:11Z","pypi_version":"6.0.6"} \ No newline at end of file diff --git a/tox.ini b/tox.ini index 75ebe1345..193b5813f 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps = {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 pytest-django==2.8.0 - {py27,py32,py32,py33,py34}-django{14,15,16,17}: django-filter==0.7 + {py26,py27,py32,py33,py34}-django{14,15,16,17}: django-filter==0.7 defusedxml==0.3 markdown>=2.1.0 PyYAML>=3.10 From e988d578535fcc820d30dc7c59f1e24f5c911d3c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:47:01 +0000 Subject: [PATCH 094/301] Fix template loader monkey patching to also support 1.8 --- tests/test_htmlrenderer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py index 2edc6b4bd..a33b832f5 100644 --- a/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -56,7 +56,13 @@ class TemplateHTMLRendererTests(TestCase): return Template("example: {{ object }}") raise TemplateDoesNotExist(template_name) + def select_template(template_name_list, dirs=None, using=None): + if template_name_list == ['example.html']: + return Template("example: {{ object }}") + raise TemplateDoesNotExist(template_name_list[0]) + django.template.loader.get_template = get_template + django.template.loader.select_template = select_template def tearDown(self): """ From 4cb164b66c0784ce79054925d4744deb5b18d8b2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 11:49:57 +0000 Subject: [PATCH 095/301] Add missing skipUnless(django_filters) --- tests/test_filters.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index dc84dcbd0..5b1b6ca52 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -467,6 +467,7 @@ class DjangoFilterOrderingTests(TestCase): for d in data: DjangoFilterOrderingModel.objects.create(**d) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_default_ordering(self): class DjangoFilterOrderingView(generics.ListAPIView): serializer_class = DjangoFilterOrderingSerializer From f1ac9d3f9b6c306b7fa48381006d8259c1642a99 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 12:26:44 +0000 Subject: [PATCH 096/301] More graceful handling of malformed Content-Disposition --- rest_framework/parsers.py | 2 +- tests/test_parsers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 401856ec4..ef72677ce 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -298,7 +298,7 @@ class FileUploadParser(BaseParser): if 'filename*' in filename_parm: return self.get_encoded_filename(filename_parm) return force_text(filename_parm['filename']) - except (AttributeError, KeyError): + except (AttributeError, KeyError, ValueError): pass def get_encoded_filename(self, filename_parm): diff --git a/tests/test_parsers.py b/tests/test_parsers.py index d28d8bd43..1d2054aca 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -161,7 +161,9 @@ class TestFileUploadParser(TestCase): self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') filename = parser.get_filename(self.stream, None, self.parser_context) - self.assertEqual(filename, 'fallback.txt') + # Malformed. Either None or 'fallback.txt' will be acceptable. + # See also https://code.djangoproject.com/ticket/24209 + self.assertIn(filename, ('fallback.txt', None)) def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition From f3b6eedb8aeaa23f4b48551356814837973db31c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 12:56:55 +0000 Subject: [PATCH 097/301] More sensible response caching. --- rest_framework/response.py | 7 +++- tests/test_renderers.py | 85 ++++++-------------------------------- 2 files changed, 17 insertions(+), 75 deletions(-) diff --git a/rest_framework/response.py b/rest_framework/response.py index d6ca1aad4..7f90bae10 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -81,10 +81,13 @@ class Response(SimpleTemplateResponse): def __getstate__(self): """ - Remove attributes from the response that shouldn't be cached + Remove attributes from the response that shouldn't be cached. """ state = super(Response, self).__getstate__() - for key in ('accepted_renderer', 'renderer_context', 'data'): + for key in ( + 'accepted_renderer', 'renderer_context', 'resolver_match', + 'client', 'request', 'wsgi_request', '_closable_objects' + ): if key in state: del state[key] return state diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 00a24fb12..54eea8ceb 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -22,7 +22,6 @@ from rest_framework.test import APIRequestFactory from collections import MutableMapping import datetime import json -import pickle import re @@ -618,84 +617,24 @@ class CacheRenderTest(TestCase): urls = 'tests.test_renderers' - cache_key = 'just_a_cache_key' - - @classmethod - def _get_pickling_errors(cls, obj, seen=None): - """ Return any errors that would be raised if `obj' is pickled - Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897 - """ - if seen is None: - seen = [] - try: - state = obj.__getstate__() - except AttributeError: - return - if state is None: - return - if isinstance(state, tuple): - if not isinstance(state[0], dict): - state = state[1] - else: - state = state[0].update(state[1]) - result = {} - for i in state: - try: - pickle.dumps(state[i], protocol=2) - except pickle.PicklingError: - if not state[i] in seen: - seen.append(state[i]) - result[i] = cls._get_pickling_errors(state[i], seen) - return result - - def http_resp(self, http_method, url): - """ - Simple wrapper for Client http requests - Removes the `client' and `request' attributes from as they are - added by django.test.client.Client and not part of caching - responses outside of tests. - """ - method = getattr(self.client, http_method) - resp = method(url) - resp._closable_objects = [] - del resp.client, resp.request - try: - del resp.wsgi_request - except AttributeError: - pass - return resp - - def test_obj_pickling(self): - """ - Test that responses are properly pickled - """ - resp = self.http_resp('get', '/cache') - - # Make sure that no pickling errors occurred - self.assertEqual(self._get_pickling_errors(resp), {}) - - # Unfortunately LocMem backend doesn't raise PickleErrors but returns - # None instead. - cache.set(self.cache_key, resp) - self.assertTrue(cache.get(self.cache_key) is not None) - def test_head_caching(self): """ Test caching of HEAD requests """ - resp = self.http_resp('head', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) + response = self.client.head('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code def test_get_caching(self): """ Test caching of GET requests """ - resp = self.http_resp('get', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) - self.assertEqual(cached_resp.content, resp.content) + response = self.client.get('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code From 04a5f7bf0a268d24656ef3659f85aec95fd7590a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 13:04:29 +0000 Subject: [PATCH 098/301] Move 1.8-alpha out of 'expected failures' --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 996c3ae80..28ebfc00f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,10 +33,6 @@ env: matrix: fast_finish: true allow_failures: - - env: TOX_ENV=py34-django18alpha - - env: TOX_ENV=py33-django18alpha - - env: TOX_ENV=py32-django18alpha - - env: TOX_ENV=py27-django18alpha - env: TOX_ENV=py34-djangomaster - env: TOX_ENV=py33-djangomaster - env: TOX_ENV=py32-djangomaster From 4201c9fb01beae84fc34a5b74e138e721de42de1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 13:42:56 +0000 Subject: [PATCH 099/301] Drop erronous check-in --- env/pip-selfcheck.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json deleted file mode 100644 index 50cde9566..000000000 --- a/env/pip-selfcheck.json +++ /dev/null @@ -1 +0,0 @@ -{"last_check":"2015-01-23T11:37:11Z","pypi_version":"6.0.6"} \ No newline at end of file From bf0b331e8f3d03d995d87a6f71fea2dc05880509 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 23 Jan 2015 15:21:51 +0100 Subject: [PATCH 100/301] Restore DF for Django 1.8 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 193b5813f..8e0369643 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps = {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 pytest-django==2.8.0 - {py26,py27,py32,py33,py34}-django{14,15,16,17}: django-filter==0.7 + django-filter==0.9.2 defusedxml==0.3 markdown>=2.1.0 PyYAML>=3.10 From 790c92d438f85c93d8c3cf626347915c65c8384d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 23 Jan 2015 15:22:20 +0100 Subject: [PATCH 101/301] Update Django-Filter references in docs and requirements. --- docs/index.md | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index d40f8972f..c11107883 100644 --- a/docs/index.md +++ b/docs/index.md @@ -58,7 +58,7 @@ 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-filter][django-filter] (0.9.2+) - 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-guardian][django-guardian] (1.1.1+) - Object level permissions support. diff --git a/requirements.txt b/requirements.txt index 43e947c43..00d973cdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-guardian==1.2.4 -django-filter>=0.5.4 +django-filter>=0.9.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 From 8f25c0c53c24c88afc86d99bbb3ca4edc3a4e0a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 14:56:15 +0000 Subject: [PATCH 102/301] Add 1.8 support --- 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 b91ecebc3..d9a67441d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,7 +12,7 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import unicode_to_repr from rest_framework.utils import model_meta @@ -1231,7 +1231,9 @@ class ModelSerializer(Serializer): continue try: - model_fields[source] = model._meta.get_field(source) + field = model._meta.get_field(source) + if isinstance(field, DjangoModelField): + model_fields[source] = field except FieldDoesNotExist: pass From e8db1834d3a3f6ba05276b64e5681288aa8f9820 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 15:24:06 +0000 Subject: [PATCH 103/301] Added UUIDField. --- rest_framework/fields.py | 18 ++++++++++++++++++ rest_framework/serializers.py | 8 +++++++- tests/test_fields.py | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cc9410aa7..5e3f7ce4f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -23,6 +23,7 @@ import datetime import decimal import inspect import re +import uuid class empty: @@ -632,6 +633,23 @@ class URLField(CharField): self.validators.append(validator) +class UUIDField(Field): + default_error_messages = { + 'invalid': _('"{value}" is not a valid UUID.'), + } + + def to_internal_value(self, data): + if not isinstance(data, uuid.UUID): + try: + return uuid.UUID(data) + except (ValueError, TypeError): + self.fail('invalid', value=data) + return data + + def to_representation(self, value): + return str(value) + + # Number types... class IntegerField(Field): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cf797bdcb..dca612cad 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -702,6 +702,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({ models.AutoField: IntegerField, models.BigIntegerField: IntegerField, @@ -724,7 +725,8 @@ class ModelSerializer(Serializer): models.SmallIntegerField: IntegerField, models.TextField: CharField, models.TimeField: TimeField, - models.URLField: URLField, + models.URLField: URLField + # Note: Some version-specific mappings also defined below. }) _related_class = PrimaryKeyRelatedField @@ -1132,6 +1134,10 @@ class ModelSerializer(Serializer): return NestedSerializer +if hasattr(models, 'UUIDField'): + ModelSerializer._field_mapping[models.UUIDField] = UUIDField + + class HyperlinkedModelSerializer(ModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead diff --git a/tests/test_fields.py b/tests/test_fields.py index 775d46184..a46cc2052 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,6 +4,7 @@ from rest_framework import serializers import datetime import django import pytest +import uuid # Tests for field keyword arguments and core functionality. @@ -467,6 +468,23 @@ class TestURLField(FieldValues): field = serializers.URLField() +class TestUUIDField(FieldValues): + """ + Valid and invalid values for `UUIDField`. + """ + valid_inputs = { + '825d7aeb-05a9-45b5-a5b7-05df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'), + '825d7aeb05a945b5a5b705df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda') + } + invalid_inputs = { + '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.'] + } + outputs = { + uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'): '825d7aeb-05a9-45b5-a5b7-05df87923cda' + } + field = serializers.UUIDField() + + # Number types... class TestIntegerField(FieldValues): From 5bb348605e5dad3b58495b1fc56ea393194b89fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 15:31:08 +0000 Subject: [PATCH 104/301] UUIDField docs --- docs/api-guide/fields.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b3d274ddb..64ec902b4 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -182,6 +182,12 @@ Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.v **Signature:** `URLField(max_length=200, min_length=None, allow_blank=False)` +## UUIDField + +A field that ensures the input is a valid UUID string. The `to_internal_value` method will return a `uuid.UUID` instance. On output the field will return a string in the canonical hyphenated format, for example: + + "de305d54-75b4-431b-adb2-eb6b9e546013" + --- # Numeric fields @@ -320,7 +326,7 @@ Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, alth ## MultipleChoiceField -A field that can accept a set of zero, one or many values, chosen from a limited set of choices. Takes a single mandatory argument. `to_internal_representation` returns a `set` containing the selected values. +A field that can accept a set of zero, one or many values, chosen from a limited set of choices. Takes a single mandatory argument. `to_internal_value` returns a `set` containing the selected values. **Signature:** `MultipleChoiceField(choices)` From 889a07f5563a0f970639a0958c0dcbc26e82919f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 15:32:21 +0000 Subject: [PATCH 105/301] Support assignment in ClassLookupDict --- rest_framework/utils/field_mapping.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index cba40d318..c97ec5d0e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -38,6 +38,9 @@ class ClassLookupDict(object): return self.mapping[cls] raise KeyError('Class %s not found in lookup.', cls.__name__) + def __setitem__(self, key, value): + self.mapping[key] = value + def needs_label(model_field, field_name): """ From 35f6a8246299d31ecce4f791f9527bf34cebe6e2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 23 Jan 2015 16:27:23 +0000 Subject: [PATCH 106/301] Added DictField and support for HStoreField. --- docs/api-guide/fields.md | 19 ++++++++++- rest_framework/compat.py | 7 +++++ rest_framework/fields.py | 59 +++++++++++++++++++++++++++++++++-- rest_framework/serializers.py | 8 ++++- tests/test_fields.py | 51 +++++++++++++++++++++++++++++- 5 files changed, 139 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 64ec902b4..1c78a42b6 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -380,7 +380,7 @@ A field class that validates a list of objects. **Signature**: `ListField(child)` -- `child` - A field instance that should be used for validating the objects in the list. +- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated. For example, to validate a list of integers you might use something like the following: @@ -395,6 +395,23 @@ The `ListField` class also supports a declarative style that allows you to write We can now reuse our custom `StringListField` class throughout our application, without having to provide a `child` argument to it. +## DictField + +A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values. + +**Signature**: `DictField(child)` + +- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated. + +For example, to create a field that validates a mapping of strings to strings, you would write something like this: + + document = DictField(child=CharField()) + +You can also use the declarative style, as with `ListField`. For example: + + class DocumentField(DictField): + child = CharField() + --- # Miscellaneous fields diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 766afaec2..364133940 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -58,6 +58,13 @@ except ImportError: from django.http import HttpResponse as HttpResponseBase +# contrib.postgres only supported from 1.8 onwards. +try: + from django.contrib.postgres import fields as postgres_fields +except ImportError: + postgres_fields = None + + # request only provides `resolver_match` from 1.5 onwards. def get_resolver_match(request): try: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5e3f7ce4f..71a9f1938 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1132,8 +1132,21 @@ class ImageField(FileField): # Composite field types... +class _UnvalidatedField(Field): + def __init__(self, *args, **kwargs): + super(_UnvalidatedField, self).__init__(*args, **kwargs) + self.allow_blank = True + self.allow_null = True + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + return value + + class ListField(Field): - child = None + child = _UnvalidatedField() initial = [] default_error_messages = { 'not_a_list': _('Expected a list of items but got type `{input_type}`') @@ -1141,7 +1154,6 @@ class ListField(Field): def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) - assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' super(ListField, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) @@ -1170,6 +1182,49 @@ class ListField(Field): return [self.child.to_representation(item) for item in data] +class DictField(Field): + child = _UnvalidatedField() + initial = [] + default_error_messages = { + 'not_a_dict': _('Expected a dictionary of items but got type `{input_type}`') + } + + def __init__(self, *args, **kwargs): + self.child = kwargs.pop('child', copy.deepcopy(self.child)) + assert not inspect.isclass(self.child), '`child` has not been instantiated.' + super(DictField, self).__init__(*args, **kwargs) + self.child.bind(field_name='', parent=self) + + def get_value(self, dictionary): + # We override the default field access in order to support + # lists in HTML forms. + if html.is_html_input(dictionary): + return html.parse_html_list(dictionary, prefix=self.field_name) + return dictionary.get(self.field_name, empty) + + def to_internal_value(self, data): + """ + Dicts of native values <- Dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_dict(data) + if not isinstance(data, dict): + self.fail('not_a_dict', input_type=type(data).__name__) + return dict([ + (six.text_type(key), self.child.run_validation(value)) + for key, value in data.items() + ]) + + def to_representation(self, value): + """ + List of object instances -> List of dicts of primitive datatypes. + """ + return dict([ + (six.text_type(key), self.child.to_representation(val)) + for key, val in value.items() + ]) + + # Miscellaneous field types... class ReadOnlyField(Field): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index dca612cad..42d1e3700 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,7 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoField from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import unicode_to_repr +from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, @@ -1137,6 +1137,12 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): ModelSerializer._field_mapping[models.UUIDField] = UUIDField +if postgres_fields: + class CharMappingField(DictField): + child = CharField() + + ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + class HyperlinkedModelSerializer(ModelSerializer): """ diff --git a/tests/test_fields.py b/tests/test_fields.py index a46cc2052..6744cf645 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1047,7 +1047,7 @@ class TestValidImageField(FieldValues): class TestListField(FieldValues): """ - Values for `ListField`. + Values for `ListField` with IntegerField as child. """ valid_inputs = [ ([1, 2, 3], [1, 2, 3]), @@ -1064,6 +1064,55 @@ class TestListField(FieldValues): field = serializers.ListField(child=serializers.IntegerField()) +class TestUnvalidatedListField(FieldValues): + """ + Values for `ListField` with no `child` argument. + """ + valid_inputs = [ + ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]), + ] + invalid_inputs = [ + ('not a list', ['Expected a list of items but got type `str`']), + ] + outputs = [ + ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]), + ] + field = serializers.ListField() + + +class TestDictField(FieldValues): + """ + Values for `ListField` with CharField as child. + """ + valid_inputs = [ + ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}), + ] + invalid_inputs = [ + ({'a': 1, 'b': None}, ['This field may not be null.']), + ('not a dict', ['Expected a dictionary of items but got type `str`']), + ] + outputs = [ + ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}), + ] + field = serializers.DictField(child=serializers.CharField()) + + +class TestUnvalidatedDictField(FieldValues): + """ + Values for `ListField` with no `child` argument. + """ + valid_inputs = [ + ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}), + ] + invalid_inputs = [ + ('not a dict', ['Expected a dictionary of items but got type `str`']), + ] + outputs = [ + ({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}), + ] + field = serializers.DictField() + + # Tests for FieldField. # --------------------- From a1fa7218ebc4a77a3912c42221927b1846f555fd Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Fri, 23 Jan 2015 16:48:23 +0000 Subject: [PATCH 107/301] Pass {} as data to DataAndFiles, as it ends up in a MergeDict In the same vein as #2399. --- rest_framework/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index ef72677ce..1efab85ba 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -250,7 +250,7 @@ class FileUploadParser(BaseParser): None, encoding) if result is not None: - return DataAndFiles(None, {'file': result[1]}) + return DataAndFiles({}, {'file': result[1]}) # This is the standard case. possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] From b09ef28959fe63351f0dd24564b7d2d344b44fa3 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Sat, 24 Jan 2015 01:37:23 -0800 Subject: [PATCH 108/301] Add failing test for request.version AttributeError in BrowsableAPI. --- tests/browsable_api/auth_urls.py | 9 +++++++- tests/browsable_api/test_browsable_api.py | 10 +++++++++ tests/browsable_api/views.py | 27 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py index bce7dcf91..098a99acc 100644 --- a/tests/browsable_api/auth_urls.py +++ b/tests/browsable_api/auth_urls.py @@ -1,10 +1,17 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url, include +from rest_framework import routers -from .views import MockView +from .views import MockView, FooViewSet, BarViewSet + +router = routers.SimpleRouter() +router.register(r'foo', FooViewSet) +router.register(r'bar', BarViewSet) urlpatterns = patterns( '', (r'^$', MockView.as_view()), + url(r'^', include(router.urls)), + url(r'^bar/(?P\d+)/$', BarViewSet, name='bar-list'), url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), ) diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 5f2647838..31907f84e 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.test import TestCase from rest_framework.test import APIClient +from .models import Foo, Bar class DropdownWithAuthTests(TestCase): @@ -16,6 +17,8 @@ class DropdownWithAuthTests(TestCase): self.email = 'lennon@thebeatles.com' self.password = 'password' self.user = User.objects.create_user(self.username, self.email, self.password) + foo = Foo.objects.create(name='Foo') + Bar.objects.create(foo=foo) def tearDown(self): self.client.logout() @@ -25,6 +28,13 @@ class DropdownWithAuthTests(TestCase): response = self.client.get('/') self.assertContains(response, 'john') + def test_bug_2455_clone_request(self): + self.client.login(username=self.username, password=self.password) + json_response = self.client.get('/foo/1/?format=json') + self.assertEqual(json_response.status_code, 200) + browsable_api_response = self.client.get('/foo/1/') + self.assertEqual(browsable_api_response.status_code, 200) + def test_logout_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py index 000f4e804..f06f7c40a 100644 --- a/tests/browsable_api/views.py +++ b/tests/browsable_api/views.py @@ -1,9 +1,14 @@ from __future__ import unicode_literals from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet from rest_framework import authentication from rest_framework import renderers from rest_framework.response import Response +from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer +from rest_framework.versioning import NamespaceVersioning +from .models import Foo, Bar +from .serializers import FooSerializer, BarSerializer class MockView(APIView): @@ -13,3 +18,25 @@ class MockView(APIView): def get(self, request): return Response({'a': 1, 'b': 2, 'c': 3}) + + +class SerializerClassMixin(object): + def get_serializer_class(self): + # Get base name of serializer + self.request.version + return self.serializer_class + + +class FooViewSet(SerializerClassMixin, ModelViewSet): + versioning_class = NamespaceVersioning + model = Foo + queryset = Foo.objects.all() + serializer_class = FooSerializer + renderer_classes = (BrowsableAPIRenderer, JSONRenderer) + + +class BarViewSet(SerializerClassMixin, ModelViewSet): + model = Bar + queryset = Bar.objects.all() + serializer_class = BarSerializer + renderer_classes = (BrowsableAPIRenderer, ) From 0ee2edc0a14c4d14b8aa6e4b63ccbd0c2cc78024 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Sat, 24 Jan 2015 01:44:09 -0800 Subject: [PATCH 109/301] Add missed files for test. --- tests/browsable_api/models.py | 9 +++++++++ tests/browsable_api/serializers.py | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 tests/browsable_api/models.py create mode 100644 tests/browsable_api/serializers.py diff --git a/tests/browsable_api/models.py b/tests/browsable_api/models.py new file mode 100644 index 000000000..05c6c23b4 --- /dev/null +++ b/tests/browsable_api/models.py @@ -0,0 +1,9 @@ +from django.db import models + + +class Foo(models.Model): + name = models.CharField(max_length=30) + + +class Bar(models.Model): + foo = models.ForeignKey("Foo", editable=False) diff --git a/tests/browsable_api/serializers.py b/tests/browsable_api/serializers.py new file mode 100644 index 000000000..e83645404 --- /dev/null +++ b/tests/browsable_api/serializers.py @@ -0,0 +1,14 @@ +from .models import Foo, Bar +from rest_framework.serializers import HyperlinkedModelSerializer, HyperlinkedIdentityField + + +class FooSerializer(HyperlinkedModelSerializer): + bar = HyperlinkedIdentityField(view_name='bar-list') + + class Meta: + model = Foo + + +class BarSerializer(HyperlinkedModelSerializer): + class Meta: + model = Bar From ed04725822d5dc9a90c9c6e5c14d85083ae6ff28 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Sat, 24 Jan 2015 01:44:40 -0800 Subject: [PATCH 110/301] Use enhanced request when cloning requests for checking permissions on other methods. Fixes #2455 --- rest_framework/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index cfbbdeccd..d5b56ada1 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -86,7 +86,7 @@ def clone_request(request, method): Internal helper method to clone a request, replacing with a different HTTP method. Used for checking permissions against other methods. """ - ret = Request(request=request._request, + ret = Request(request=request, parsers=request.parsers, authenticators=request.authenticators, negotiator=request.negotiator, From 6c083b12a1162bf8e0f51e6c52ff13a1bd621cf2 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Sat, 24 Jan 2015 11:00:36 -0800 Subject: [PATCH 111/301] Streamline test for #2455 --- tests/browsable_api/auth_urls.py | 8 +------ tests/browsable_api/models.py | 9 -------- tests/browsable_api/serializers.py | 14 ------------ tests/browsable_api/test_browsable_api.py | 10 --------- tests/browsable_api/views.py | 27 ----------------------- tests/test_metadata.py | 15 +++++++++++++ 6 files changed, 16 insertions(+), 67 deletions(-) delete mode 100644 tests/browsable_api/models.py delete mode 100644 tests/browsable_api/serializers.py diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py index 098a99acc..97bc10360 100644 --- a/tests/browsable_api/auth_urls.py +++ b/tests/browsable_api/auth_urls.py @@ -1,17 +1,11 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url, include -from rest_framework import routers -from .views import MockView, FooViewSet, BarViewSet +from .views import MockView -router = routers.SimpleRouter() -router.register(r'foo', FooViewSet) -router.register(r'bar', BarViewSet) urlpatterns = patterns( '', (r'^$', MockView.as_view()), - url(r'^', include(router.urls)), - url(r'^bar/(?P\d+)/$', BarViewSet, name='bar-list'), url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), ) diff --git a/tests/browsable_api/models.py b/tests/browsable_api/models.py deleted file mode 100644 index 05c6c23b4..000000000 --- a/tests/browsable_api/models.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - - -class Foo(models.Model): - name = models.CharField(max_length=30) - - -class Bar(models.Model): - foo = models.ForeignKey("Foo", editable=False) diff --git a/tests/browsable_api/serializers.py b/tests/browsable_api/serializers.py deleted file mode 100644 index e83645404..000000000 --- a/tests/browsable_api/serializers.py +++ /dev/null @@ -1,14 +0,0 @@ -from .models import Foo, Bar -from rest_framework.serializers import HyperlinkedModelSerializer, HyperlinkedIdentityField - - -class FooSerializer(HyperlinkedModelSerializer): - bar = HyperlinkedIdentityField(view_name='bar-list') - - class Meta: - model = Foo - - -class BarSerializer(HyperlinkedModelSerializer): - class Meta: - model = Bar diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 31907f84e..5f2647838 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -3,7 +3,6 @@ from django.contrib.auth.models import User from django.test import TestCase from rest_framework.test import APIClient -from .models import Foo, Bar class DropdownWithAuthTests(TestCase): @@ -17,8 +16,6 @@ class DropdownWithAuthTests(TestCase): self.email = 'lennon@thebeatles.com' self.password = 'password' self.user = User.objects.create_user(self.username, self.email, self.password) - foo = Foo.objects.create(name='Foo') - Bar.objects.create(foo=foo) def tearDown(self): self.client.logout() @@ -28,13 +25,6 @@ class DropdownWithAuthTests(TestCase): response = self.client.get('/') self.assertContains(response, 'john') - def test_bug_2455_clone_request(self): - self.client.login(username=self.username, password=self.password) - json_response = self.client.get('/foo/1/?format=json') - self.assertEqual(json_response.status_code, 200) - browsable_api_response = self.client.get('/foo/1/') - self.assertEqual(browsable_api_response.status_code, 200) - def test_logout_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py index f06f7c40a..000f4e804 100644 --- a/tests/browsable_api/views.py +++ b/tests/browsable_api/views.py @@ -1,14 +1,9 @@ from __future__ import unicode_literals from rest_framework.views import APIView -from rest_framework.viewsets import ModelViewSet from rest_framework import authentication from rest_framework import renderers from rest_framework.response import Response -from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer -from rest_framework.versioning import NamespaceVersioning -from .models import Foo, Bar -from .serializers import FooSerializer, BarSerializer class MockView(APIView): @@ -18,25 +13,3 @@ class MockView(APIView): def get(self, request): return Response({'a': 1, 'b': 2, 'c': 3}) - - -class SerializerClassMixin(object): - def get_serializer_class(self): - # Get base name of serializer - self.request.version - return self.serializer_class - - -class FooViewSet(SerializerClassMixin, ModelViewSet): - versioning_class = NamespaceVersioning - model = Foo - queryset = Foo.objects.all() - serializer_class = FooSerializer - renderer_classes = (BrowsableAPIRenderer, JSONRenderer) - - -class BarViewSet(SerializerClassMixin, ModelViewSet): - model = Bar - queryset = Bar.objects.all() - serializer_class = BarSerializer - renderer_classes = (BrowsableAPIRenderer, ) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 972a896a4..bdc84edf1 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from rest_framework import exceptions, serializers, status, views from rest_framework.request import Request +from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.test import APIRequestFactory request = Request(APIRequestFactory().options('/')) @@ -168,3 +169,17 @@ class TestMetadata: response = view(request=request) assert response.status_code == status.HTTP_200_OK assert list(response.data['actions'].keys()) == ['POST'] + + def test_bug_2455_clone_request(self): + class ExampleView(views.APIView): + renderer_classes = (BrowsableAPIRenderer,) + + def post(self, request): + pass + + def get_serializer(self): + assert hasattr(self.request, 'version') + return serializers.Serializer() + + view = ExampleView.as_view() + view(request=request) From 39da9c7c865533d580ea410458aeb366835b18cc Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Sat, 24 Jan 2015 12:53:21 -0800 Subject: [PATCH 112/301] minor typo in viewsets docs --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 3e37cef89..b09dfc9e9 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -146,7 +146,7 @@ The decorators can additionally take extra arguments that will be set for the ro def set_password(self, request, pk=None): ... -Theses decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: +These decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @detail_route(methods=['post', 'delete']) def unset_password(self, request, pk=None): From 0a65913fea471e7545896bd88760be8b26a3225e Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Sat, 24 Jan 2015 18:34:16 -0800 Subject: [PATCH 113/301] typo in fields.md --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 1c78a42b6..10291c12a 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -461,7 +461,7 @@ This is a read-only field. It gets its value by calling a method on the serializ **Signature**: `SerializerMethodField(method_name=None)` -- `method-name` - The name of the method on the serializer to be called. If not included this defaults to `get_`. +- `method_name` - The name of the method on the serializer to be called. If not included this defaults to `get_`. The serializer method referred to by the `method_name` argument should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: From 90c9968a70e0a3d14cf4433cd356bcbdd30fce1b Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:45:56 -0800 Subject: [PATCH 114/301] tutorial #1 incorrectly showed string of json instead of ReturnDict type from 'serializer.data', and also has a third item in the second usage --- docs/tutorial/1-serialization.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 80e869ea6..458161d07 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -151,7 +151,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # ReturnDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. @@ -182,7 +182,8 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}] + # [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + ## Using ModelSerializers From 2a6937f381fe514e6cc9165c0aee200bf145788f Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:46:27 -0800 Subject: [PATCH 115/301] tutorial #2 incorrectly showed /item.json instead of /item/.json for format suffixes --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index c04269695..9315a6644 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -96,7 +96,7 @@ Notice that we're no longer explicitly tying our requests or responses to a give ## Adding optional format suffixes to our URLs -To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url]. +To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4/.json][json-url]. Start by adding a `format` keyword argument to both of the views, like so. From 73bd0d539f24d45695615c25a072175c58a4cf98 Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Sun, 25 Jan 2015 23:47:01 -0800 Subject: [PATCH 116/301] tutorial #5 incorrectly referenced 'settings.py' instead of 'tutorial/settings.py' --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 2841f03e9..740a4ce21 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -138,7 +138,7 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages. -We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting: +We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting: REST_FRAMEWORK = { 'PAGINATE_BY': 10 From fc70c0862ff3e6183b79adc4675a63874261ddf0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 26 Jan 2015 14:07:21 +0000 Subject: [PATCH 117/301] Galileo Press -> Rheinwerk Verlag --- docs/img/sponsors/2-galileo_press.png | Bin 11451 -> 0 bytes docs/img/sponsors/2-rheinwerk_verlag.png | Bin 0 -> 1562 bytes docs/topics/kickstarter-announcement.md | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 docs/img/sponsors/2-galileo_press.png create mode 100644 docs/img/sponsors/2-rheinwerk_verlag.png diff --git a/docs/img/sponsors/2-galileo_press.png b/docs/img/sponsors/2-galileo_press.png deleted file mode 100644 index f77e6c0a86cc502d25718545d7a19f4db8427045..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11451 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 zw?(X41YPulsecode Inc.
  • Singing Horse Studio Ltd.
  • Heroku
  • -
  • Galileo Press
  • +
  • Rheinwerk Verlag
  • Security Compass
  • Django Software Foundation
  • Hipflask
  • From bf58c1265ddf06deb435d049fae01ed324a310e0 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Mon, 26 Jan 2015 22:56:57 -0800 Subject: [PATCH 118/301] Set a version attribute on cloned requests if necessary. --- rest_framework/request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index d5b56ada1..ce2fcb476 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -86,7 +86,7 @@ def clone_request(request, method): Internal helper method to clone a request, replacing with a different HTTP method. Used for checking permissions against other methods. """ - ret = Request(request=request, + ret = Request(request=request._request, parsers=request.parsers, authenticators=request.authenticators, negotiator=request.negotiator, @@ -107,6 +107,8 @@ def clone_request(request, method): ret.accepted_renderer = request.accepted_renderer if hasattr(request, 'accepted_media_type'): ret.accepted_media_type = request.accepted_media_type + if hasattr(request, 'version'): + ret.version = request.version return ret From 65bca59ea548dc5e2222be06ca20b3d3fa151cf0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 27 Jan 2015 13:51:30 +0000 Subject: [PATCH 119/301] Reload api_settings when using Django's 'override_settings' --- rest_framework/settings.py | 11 +++++++++++ tests/test_filters.py | 16 +++++++++++++--- tests/utils.py | 25 ------------------------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index fc6dfecda..e5e5edafe 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,6 +18,7 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals +from django.test.signals import setting_changed from django.conf import settings from django.utils import importlib, six from rest_framework import ISO_8601 @@ -198,3 +199,13 @@ class APISettings(object): api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) + + +def reload_api_settings(*args, **kwargs): + global api_settings + setting, value = kwargs['setting'], kwargs['value'] + if setting == 'REST_FRAMEWORK': + api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS) + + +setting_changed.connect(reload_api_settings) diff --git a/tests/test_filters.py b/tests/test_filters.py index 5b1b6ca52..355f02cef 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,13 +5,15 @@ from django.db import models from django.conf.urls import patterns, url from django.core.urlresolvers import reverse from django.test import TestCase +from django.test.utils import override_settings from django.utils import unittest from django.utils.dateparse import parse_date +from django.utils.six.moves import reload_module from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters from rest_framework.test import APIRequestFactory from .models import BaseFilterableItem, FilterableItem, BasicModel -from .utils import temporary_setting + factory = APIRequestFactory() @@ -404,7 +406,9 @@ class SearchFilterTests(TestCase): ) def test_search_with_nonstandard_search_param(self): - with temporary_setting('SEARCH_PARAM', 'query', module=filters): + with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}): + reload_module(filters) + class SearchListView(generics.ListAPIView): queryset = SearchFilterModel.objects.all() serializer_class = SearchFilterSerializer @@ -422,6 +426,8 @@ class SearchFilterTests(TestCase): ] ) + reload_module(filters) + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20) @@ -642,7 +648,9 @@ class OrderingFilterTests(TestCase): ) def test_ordering_with_nonstandard_ordering_param(self): - with temporary_setting('ORDERING_PARAM', 'order', filters): + with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}): + reload_module(filters) + class OrderingListView(generics.ListAPIView): queryset = OrderingFilterModel.objects.all() serializer_class = OrderingFilterSerializer @@ -662,6 +670,8 @@ class OrderingFilterTests(TestCase): ] ) + reload_module(filters) + class SensitiveOrderingFilterModel(models.Model): username = models.CharField(max_length=20) diff --git a/tests/utils.py b/tests/utils.py index 5e902ba94..5b2d75864 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,30 +1,5 @@ -from contextlib import contextmanager from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import NoReverseMatch -from django.utils import six -from rest_framework.settings import api_settings - - -@contextmanager -def temporary_setting(setting, value, module=None): - """ - Temporarily change value of setting for test. - - Optionally reload given module, useful when module uses value of setting on - import. - """ - original_value = getattr(api_settings, setting) - setattr(api_settings, setting, value) - - if module is not None: - six.moves.reload_module(module) - - yield - - setattr(api_settings, setting, original_value) - - if module is not None: - six.moves.reload_module(module) class MockObject(object): From 925ea4bdc6d40a1e118087f28a09c86977dc5532 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 27 Jan 2015 19:43:38 +0100 Subject: [PATCH 120/301] Release notes for 3.0.4 --- docs/topics/release-notes.md | 38 ++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c49dd62c9..e0894d2d9 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -41,6 +41,24 @@ You can determine your currently installed version using `pip freeze`: ## 3.0.x series +### 3.0.4 + +**Date**: [28th January 2015][3.0.4-milestone]. + +* Django 1.8a1 support. ([#2425][gh2425], [#2446][gh2446], [#2441][gh2441]) +* Add `DictField` and support Django 1.8 `HStoreField`. ([#2451][gh2451], [#2106][gh2106]) +* Add `UUIDField` and support Django 1.8 `UUIDField`. ([#2448][gh2448], [#2433][gh2433], [#2432][gh2432]) +* `BaseRenderer.render` now raises `NotImplementedError`. ([#2434][gh2434]) +* Fix timedelta JSON serialization on Python 2.6. ([#2430][gh2430]) +* `ResultDict` and `ResultList` now appear as standard dict/list. ([#2421][gh2421]) +* Fix visible `HiddenField` in the HTML form of the web browsable API page. ([#2410][gh2410]) +* Use `OrderedDict` for `RelatedField.choices`. ([#2408][gh2408]) +* Fix ident format when using `HTTP_X_FORWARDED_FOR`. ([#2401][gh2401]) +* Fix invalid key with memcached while using throttling. ([#2400][gh2400]) +* Fix `FileUploadParser` with version 3.x. ([#2399][gh2399]) +* Fix the serializer inheritance. ([#2388][gh2388]) +* Fix caching issues with `ReturnDict`. ([#2360][gh2360]) + ### 3.0.3 **Date**: [8th January 2015][3.0.3-milestone]. @@ -702,6 +720,7 @@ For older release notes, [please see the GitHub repo](old-release-notes). [3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22 [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22 +[3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -770,3 +789,22 @@ For older release notes, [please see the GitHub repo](old-release-notes). [gh2355]: https://github.com/tomchristie/django-rest-framework/issues/2355 [gh2369]: https://github.com/tomchristie/django-rest-framework/issues/2369 [gh2386]: https://github.com/tomchristie/django-rest-framework/issues/2386 + +[gh2425]: https://github.com/tomchristie/django-rest-framework/issues/2425 +[gh2446]: https://github.com/tomchristie/django-rest-framework/issues/2446 +[gh2441]: https://github.com/tomchristie/django-rest-framework/issues/2441 +[gh2451]: https://github.com/tomchristie/django-rest-framework/issues/2451 +[gh2106]: https://github.com/tomchristie/django-rest-framework/issues/2106 +[gh2448]: https://github.com/tomchristie/django-rest-framework/issues/2448 +[gh2433]: https://github.com/tomchristie/django-rest-framework/issues/2433 +[gh2432]: https://github.com/tomchristie/django-rest-framework/issues/2432 +[gh2434]: https://github.com/tomchristie/django-rest-framework/issues/2434 +[gh2430]: https://github.com/tomchristie/django-rest-framework/issues/2430 +[gh2421]: https://github.com/tomchristie/django-rest-framework/issues/2421 +[gh2410]: https://github.com/tomchristie/django-rest-framework/issues/2410 +[gh2408]: https://github.com/tomchristie/django-rest-framework/issues/2408 +[gh2401]: https://github.com/tomchristie/django-rest-framework/issues/2401 +[gh2400]: https://github.com/tomchristie/django-rest-framework/issues/2400 +[gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399 +[gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388 +[gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360 From 5b369bf5fe3e5e8af3a73055b3a6ebda1e88f68e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 27 Jan 2015 19:45:37 +0100 Subject: [PATCH 121/301] Bumped the version. --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fdcebb7b7..57e5421b8 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.0.3' +__version__ = '3.0.4' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' From 8c3f82fb18a58b8e0983612ef3cc35b3c3950b66 Mon Sep 17 00:00:00 2001 From: Susan Dreher Date: Tue, 27 Jan 2015 16:18:51 -0500 Subject: [PATCH 122/301] :bug: ManyRelatedField get_value clearing field on partial update A PATCH to a serializer's non-related CharField was clearing an ancillary StringRelatedField(many=True) field. The issue appears to be in the ManyRelatedField's get_value method, which was returning a [] instead of empty when the request data was a MultiDict. This fix mirrors code in fields.py, class Field, get_value, Ln. 272, which explicitly returns empty on a partial update. Tests added to demonstrate the issue. --- rest_framework/relations.py | 5 +++++ tests/test_relations.py | 44 +++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index aa0c2defe..13793f375 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -338,7 +338,12 @@ class ManyRelatedField(Field): # We override the default field access in order to support # lists in HTML forms. if html.is_html_input(dictionary): + # Don't return [] if the update is partial + if self.field_name not in dictionary: + if getattr(self.root, 'partial', False): + return empty return dictionary.getlist(self.field_name) + return dictionary.get(self.field_name, empty) def to_internal_value(self, data): diff --git a/tests/test_relations.py b/tests/test_relations.py index 62353dc25..143e835ca 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,9 +1,14 @@ -from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset -from django.core.exceptions import ImproperlyConfigured -from rest_framework import serializers -from rest_framework.test import APISimpleTestCase import pytest +from django.core.exceptions import ImproperlyConfigured +from django.utils.datastructures import MultiValueDict + +from rest_framework import serializers +from rest_framework.fields import empty +from rest_framework.test import APISimpleTestCase + +from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset + class TestStringRelatedField(APISimpleTestCase): def setUp(self): @@ -134,3 +139,34 @@ class TestSlugRelatedField(APISimpleTestCase): def test_representation(self): representation = self.field.to_representation(self.instance) assert representation == self.instance.name + + +class TestManyRelatedField(APISimpleTestCase): + def setUp(self): + self.instance = MockObject(pk=1, name='foo') + self.field = serializers.StringRelatedField(many=True) + self.field.field_name = 'foo' + + def test_get_value_regular_dictionary_full(self): + assert 'bar' == self.field.get_value({'foo': 'bar'}) + assert empty == self.field.get_value({'baz': 'bar'}) + + def test_get_value_regular_dictionary_partial(self): + setattr(self.field.root, 'partial', True) + assert 'bar' == self.field.get_value({'foo': 'bar'}) + assert empty == self.field.get_value({'baz': 'bar'}) + + def test_get_value_multi_dictionary_full(self): + mvd = MultiValueDict({'foo': ['bar1', 'bar2']}) + assert ['bar1', 'bar2'] == self.field.get_value(mvd) + + mvd = MultiValueDict({'baz': ['bar1', 'bar2']}) + assert [] == self.field.get_value(mvd) + + def test_get_value_multi_dictionary_partial(self): + setattr(self.field.root, 'partial', True) + mvd = MultiValueDict({'foo': ['bar1', 'bar2']}) + assert ['bar1', 'bar2'] == self.field.get_value(mvd) + + mvd = MultiValueDict({'baz': ['bar1', 'bar2']}) + assert empty == self.field.get_value(mvd) From 1714ceae9f468bc1479f0d7a32b0bf26ae9cf15f Mon Sep 17 00:00:00 2001 From: Susan Dreher Date: Tue, 27 Jan 2015 16:31:25 -0500 Subject: [PATCH 123/301] reorganize imports --- tests/test_relations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index 143e835ca..67f49c6b0 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,13 +1,13 @@ -import pytest +from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import MultiValueDict from rest_framework import serializers from rest_framework.fields import empty -from rest_framework.test import APISimpleTestCase -from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset +from rest_framework.test import APISimpleTestCase +import pytest class TestStringRelatedField(APISimpleTestCase): From e7da266a866adddd5c37453fab33812ee412752b Mon Sep 17 00:00:00 2001 From: Susan Dreher Date: Tue, 27 Jan 2015 16:32:15 -0500 Subject: [PATCH 124/301] reorganize imports --- tests/test_relations.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index 67f49c6b0..d478d8550 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,11 +1,8 @@ from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset - from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import MultiValueDict - from rest_framework import serializers from rest_framework.fields import empty - from rest_framework.test import APISimpleTestCase import pytest From 0daf160946db4f2fed6a237136d738d954b841c0 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 28 Jan 2015 00:06:34 +0100 Subject: [PATCH 125/301] Fix #2476 --- docs/api-guide/routers.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 9c9bfb505..a9f911a98 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -60,7 +60,7 @@ For example, you can append `router.urls` to a list of existing views… router.register(r'accounts', AccountViewSet) urlpatterns = [ - url(r'^forgot-password/$', ForgotPasswordFormView.as_view(), + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), ] urlpatterns += router.urls @@ -68,15 +68,15 @@ For example, you can append `router.urls` to a list of existing views… Alternatively you can use Django's `include` function, like so… urlpatterns = [ - url(r'^forgot-password/$', ForgotPasswordFormView.as_view(), - url(r'^', include(router.urls)) + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), + url(r'^', include(router.urls)), ] Router URL patterns can also be namespaces. urlpatterns = [ - url(r'^forgot-password/$', ForgotPasswordFormView.as_view(), - url(r'^api/', include(router.urls, namespace='api')) + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), + url(r'^api/', include(router.urls, namespace='api')), ] If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view. From ac87490b91e3405d497da360afed10842a73dfd0 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Tue, 27 Jan 2015 17:10:17 -0800 Subject: [PATCH 126/301] Clone the versioning_scheme when necessary. Fixes #2477 --- rest_framework/request.py | 2 ++ tests/test_metadata.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index ce2fcb476..bf6ff6706 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -109,6 +109,8 @@ def clone_request(request, method): ret.accepted_media_type = request.accepted_media_type if hasattr(request, 'version'): ret.version = request.version + if hasattr(request, 'versioning_scheme'): + ret.versioning_scheme = request.versioning_scheme return ret diff --git a/tests/test_metadata.py b/tests/test_metadata.py index bdc84edf1..5031c0f30 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from rest_framework import exceptions, serializers, status, views +from rest_framework import exceptions, serializers, status, views, versioning from rest_framework.request import Request from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.test import APIRequestFactory @@ -183,3 +183,18 @@ class TestMetadata: view = ExampleView.as_view() view(request=request) + + def test_bug_2477_clone_request(self): + class ExampleView(views.APIView): + renderer_classes = (BrowsableAPIRenderer,) + + def post(self, request): + pass + + def get_serializer(self): + assert hasattr(self.request, 'versioning_scheme') + return serializers.Serializer() + + scheme = versioning.QueryParameterVersioning + view = ExampleView.as_view(versioning_class=scheme) + view(request=request) From a1eba885e287f59dd269441dfebb3b3de3eea692 Mon Sep 17 00:00:00 2001 From: Lucas Wiman Date: Tue, 27 Jan 2015 19:01:40 -0800 Subject: [PATCH 127/301] Use the proper db_table argument when constructing meta --- rest_framework/authtoken/south_migrations/0001_initial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py index 926de02b1..5b927f3e5 100644 --- a/rest_framework/authtoken/south_migrations/0001_initial.py +++ b/rest_framework/authtoken/south_migrations/0001_initial.py @@ -40,7 +40,7 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, "%s.%s" % (User._meta.app_label, User._meta.module_name): { - 'Meta': {'object_name': User._meta.module_name}, + 'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)}, }, 'authtoken.token': { 'Meta': {'object_name': 'Token'}, From 4a2a36ef828ce0e687c48fdb597d343df65f0e2b Mon Sep 17 00:00:00 2001 From: mareknaskret Date: Wed, 28 Jan 2015 15:17:56 +0100 Subject: [PATCH 128/301] Update filtering.md Updated links for django-guardian package. --- docs/api-guide/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index e00560c7e..b16b6be55 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -398,8 +398,8 @@ The [django-rest-framework-filters package][django-rest-framework-filters] works [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html -[guardian]: http://pythonhosted.org/django-guardian/ -[view-permissions]: http://pythonhosted.org/django-guardian/userguide/assign.html +[guardian]: https://django-guardian.readthedocs.org/ +[view-permissions]: https://django-guardian.readthedocs.org/en/latest/userguide/assign.html [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields From ba7dca893cd55a1d5ee928c4b10878c92c44c4f5 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 29 Jan 2015 17:28:03 +0100 Subject: [PATCH 129/301] Removed router check for deprecated '.model' attribute --- rest_framework/routers.py | 10 ++-------- tests/test_routers.py | 7 ++++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 827da0340..6a4184e20 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -130,19 +130,13 @@ class SimpleRouter(BaseRouter): If `base_name` is not specified, attempt to automatically determine it from the viewset. """ - # Note that `.model` attribute on views is deprecated, although we - # enforce the deprecation on the view `get_serializer_class()` and - # `get_queryset()` methods, rather than here. - model_cls = getattr(viewset, 'model', None) queryset = getattr(viewset, 'queryset', None) - if model_cls is None and queryset is not None: - model_cls = queryset.model - assert model_cls, '`base_name` argument not specified, and could ' \ + assert queryset is not None, '`base_name` argument not specified, and could ' \ 'not automatically determine the name from the viewset, as ' \ 'it does not have a `.queryset` attribute.' - return model_cls._meta.object_name.lower() + return queryset.model._meta.object_name.lower() def get_routes(self, viewset): """ diff --git a/tests/test_routers.py b/tests/test_routers.py index 86113f5d7..948c69bbf 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -180,7 +180,7 @@ class TestLookupValueRegex(TestCase): class TestTrailingSlashIncluded(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() self.router = SimpleRouter() self.router.register(r'notes', NoteViewSet) @@ -195,7 +195,7 @@ class TestTrailingSlashIncluded(TestCase): class TestTrailingSlashRemoved(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() self.router = SimpleRouter(trailing_slash=False) self.router.register(r'notes', NoteViewSet) @@ -210,7 +210,8 @@ class TestTrailingSlashRemoved(TestCase): class TestNameableRoot(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() + self.router = DefaultRouter() self.router.root_view_name = 'nameable-root' self.router.register(r'notes', NoteViewSet) From e720927b78a31999f03bfa248329d623ce2c045c Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 29 Jan 2015 17:28:18 +0100 Subject: [PATCH 130/301] Removed deprecated '.model' docs --- docs/api-guide/generic-views.md | 8 ++------ docs/api-guide/routers.md | 2 +- docs/topics/3.0-announcement.md | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 6374e3052..61c8e8d88 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -93,17 +93,13 @@ The following attributes are used to control pagination when used with list view * `filter_backends` - A list of filter backend classes that should be used for filtering the queryset. Defaults to the same value as the `DEFAULT_FILTER_BACKENDS` setting. -**Deprecated attributes**: - -* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes. The explicit style is preferred over the `.model` shortcut, and usage of this attribute is now deprecated. - ### Methods **Base methods**: #### `get_queryset(self)` -Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute, or the default queryset for the model if the `model` shortcut is being used. +Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute. This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests. @@ -153,7 +149,7 @@ For example: #### `get_serializer_class(self)` -Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. +Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute. May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users. diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index a9f911a98..592f7d66f 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -28,7 +28,7 @@ There are two mandatory arguments to the `register()` method: Optionally, you may also specify an additional argument: -* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset. +* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `queryset` attribute of the viewset, if it has one. Note that if the viewset does not include a `queryset` attribute then you must set `base_name` when registering the viewset. The example above would generate the following URL patterns: diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 5dbc5600a..24e69c2de 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -665,7 +665,7 @@ This code *would be valid* in `2.4.3`: class Meta: model = Account -However this code *would not be valid* in `2.4.3`: +However this code *would not be valid* in `3.0`: # Missing `queryset` class AccountSerializer(serializers.Serializer): From 7cf9dea7f905ea6869148a68b4fa96cad0a347e8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Jan 2015 11:00:29 +0000 Subject: [PATCH 131/301] Docs typo. Closes #2491. --- docs/api-guide/parsers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 73e3a7057..3d44fe56e 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -128,7 +128,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword def put(self, request, filename, format=None): file_obj = request.data['file'] # ... - # do some staff with uploaded file + # do some stuff with uploaded file # ... return Response(status=204) From 760b25bc20a1434cbdd69dc0b13bacdc3bbedd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 30 Jan 2015 11:36:03 -0400 Subject: [PATCH 132/301] Fix AttributeError on renamed _field_mapping --- 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 a3b8196bd..a91fe23e0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1330,13 +1330,13 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): - ModelSerializer._field_mapping[models.UUIDField] = UUIDField + ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField if postgres_fields: class CharMappingField(DictField): child = CharField() - ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField class HyperlinkedModelSerializer(ModelSerializer): From ee2f2d6baa786e711b8b9707fc5218711c8ddc33 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Jan 2015 15:58:33 +0000 Subject: [PATCH 133/301] Added 1.8-alpha to supported list. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74bcaeefa..53140e556 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha) # Installation From 0d96cf2ca2e3298ed38e81482bcdc2664d060735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Jan 2015 16:27:49 +0000 Subject: [PATCH 134/301] Latest translation source messages. --- docs/topics/3.1-announcement.md | 16 ++- .../locale/en_US/LC_MESSAGES/django.po | 112 +++++++++--------- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index a0ad98299..3eb52f4cf 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -1,7 +1,15 @@ -# Versioning +# Django REST framework 3.1 -# Pagination +## Pagination -# Internationalization +#### Pagination controls in the browsable API. -# ModelSerializer API +#### New pagination schemes. + +#### Support for header-based pagination. + +## Versioning + +## Internationalization + +## ModelSerializer API diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po index d98225ce9..23f76ff7f 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 18:21+0000\n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -49,18 +49,6 @@ msgstr "" msgid "User inactive or deleted." msgstr "" -#: authtoken/serializers.py:20 -msgid "User account is disabled." -msgstr "" - -#: authtoken/serializers.py:23 -msgid "Unable to log in with provided credentials." -msgstr "" - -#: authtoken/serializers.py:26 -msgid "Must include \"username\" and \"password\"." -msgstr "" - #: exceptions.py:38 msgid "A server error occurred." msgstr "" @@ -101,28 +89,28 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:152 relations.py:131 relations.py:155 validators.py:77 +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 #: validators.py:155 msgid "This field is required." msgstr "" -#: fields.py:153 +#: fields.py:154 msgid "This field may not be null." msgstr "" -#: fields.py:480 fields.py:508 +#: fields.py:487 fields.py:515 msgid "\"{input}\" is not a valid boolean." msgstr "" -#: fields.py:543 +#: fields.py:550 msgid "This field may not be blank." msgstr "" -#: fields.py:544 fields.py:1252 +#: fields.py:551 fields.py:1324 msgid "Ensure this field has no more than {max_length} characters." msgstr "" -#: fields.py:545 +#: fields.py:552 msgid "Ensure this field has at least {min_length} characters." msgstr "" @@ -144,134 +132,140 @@ msgstr "" msgid "Enter a valid URL." msgstr "" -#: fields.py:640 +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 msgid "A valid integer is required." msgstr "" -#: fields.py:641 fields.py:675 fields.py:708 +#: fields.py:658 fields.py:692 fields.py:725 msgid "Ensure this value is less than or equal to {max_value}." msgstr "" -#: fields.py:642 fields.py:676 fields.py:709 +#: fields.py:659 fields.py:693 fields.py:726 msgid "Ensure this value is greater than or equal to {min_value}." msgstr "" -#: fields.py:643 fields.py:677 fields.py:713 +#: fields.py:660 fields.py:694 fields.py:730 msgid "String value too large." msgstr "" -#: fields.py:674 fields.py:707 +#: fields.py:691 fields.py:724 msgid "A valid number is required." msgstr "" -#: fields.py:710 +#: fields.py:727 msgid "Ensure that there are no more than {max_digits} digits in total." msgstr "" -#: fields.py:711 +#: fields.py:728 msgid "Ensure that there are no more than {max_decimal_places} decimal places." msgstr "" -#: fields.py:712 +#: fields.py:729 msgid "" "Ensure that there are no more than {max_whole_digits} digits before the " "decimal point." msgstr "" -#: fields.py:796 +#: fields.py:813 msgid "Datetime has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:797 +#: fields.py:814 msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:861 +#: fields.py:878 msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:862 +#: fields.py:879 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:919 +#: fields.py:936 msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:975 fields.py:1019 +#: fields.py:992 fields.py:1036 msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1020 fields.py:1121 serializers.py:476 +#: fields.py:1037 fields.py:1151 serializers.py:482 msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1050 +#: fields.py:1067 msgid "No file was submitted." msgstr "" -#: fields.py:1051 +#: fields.py:1068 msgid "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1052 +#: fields.py:1069 msgid "No filename could be determined." msgstr "" -#: fields.py:1053 +#: fields.py:1070 msgid "The submitted file is empty." msgstr "" -#: fields.py:1054 +#: fields.py:1071 msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1096 +#: fields.py:1113 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: generics.py:123 -msgid "" -"Choose a valid page number. Page numbers must be a whole number, or must be " -"the string \"last\"." +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: generics.py:128 +#: pagination.py:221 msgid "Invalid page \"{page_number}\": {message}." msgstr "" -#: relations.py:132 -msgid "Invalid pk \"{pk_value}\" - object does not exist." +#: pagination.py:442 +msgid "Invalid cursor" msgstr "" #: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:156 +#: relations.py:157 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:157 +#: relations.py:158 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:158 +#: relations.py:159 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:159 +#: relations.py:160 msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:294 +#: relations.py:295 msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:295 +#: relations.py:296 msgid "Invalid value." msgstr "" @@ -314,3 +308,15 @@ msgstr "" #: versioning.py:160 msgid "Invalid version in query parameter." msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" From 6838f17325c2149e432e4a40b945695b765f35a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 30 Jan 2015 16:40:54 +0000 Subject: [PATCH 135/301] Add built-in translations. --- .../locale/ar/LC_MESSAGES/django.mo | Bin 0 -> 4875 bytes .../locale/ar/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/cs/LC_MESSAGES/django.mo | Bin 0 -> 8848 bytes .../locale/cs/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/da/LC_MESSAGES/django.mo | Bin 0 -> 8452 bytes .../locale/da/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/de/LC_MESSAGES/django.mo | Bin 0 -> 6575 bytes .../locale/de/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/en/LC_MESSAGES/django.mo | Bin 0 -> 8553 bytes .../locale/en/LC_MESSAGES/django.po | 324 +++++++++++++++++ .../locale/en_US/LC_MESSAGES/django.mo | Bin 0 -> 378 bytes .../locale/en_US/LC_MESSAGES/django.po | 8 +- .../locale/es/LC_MESSAGES/django.mo | Bin 0 -> 8906 bytes .../locale/es/LC_MESSAGES/django.po | 327 +++++++++++++++++ .../locale/et/LC_MESSAGES/django.mo | Bin 0 -> 2018 bytes .../locale/et/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/fr/LC_MESSAGES/django.mo | Bin 0 -> 6975 bytes .../locale/fr/LC_MESSAGES/django.po | 326 +++++++++++++++++ .../locale/hu/LC_MESSAGES/django.mo | Bin 0 -> 9215 bytes .../locale/hu/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/id/LC_MESSAGES/django.mo | Bin 0 -> 497 bytes .../locale/id/LC_MESSAGES/django.po | 324 +++++++++++++++++ .../locale/it/LC_MESSAGES/django.mo | Bin 0 -> 5334 bytes .../locale/it/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/ko_KR/LC_MESSAGES/django.mo | Bin 0 -> 8555 bytes .../locale/ko_KR/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/mk/LC_MESSAGES/django.mo | Bin 0 -> 10731 bytes .../locale/mk/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/nl/LC_MESSAGES/django.mo | Bin 0 -> 499 bytes .../locale/nl/LC_MESSAGES/django.po | 324 +++++++++++++++++ .../locale/pl/LC_MESSAGES/django.mo | Bin 0 -> 8981 bytes .../locale/pl/LC_MESSAGES/django.po | 326 +++++++++++++++++ .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 0 -> 8729 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 326 +++++++++++++++++ .../locale/ru/LC_MESSAGES/django.mo | Bin 0 -> 9778 bytes .../locale/ru/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/sk/LC_MESSAGES/django.mo | Bin 0 -> 2748 bytes .../locale/sk/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/sv/LC_MESSAGES/django.mo | Bin 0 -> 8647 bytes .../locale/sv/LC_MESSAGES/django.po | 325 +++++++++++++++++ .../locale/tr/LC_MESSAGES/django.mo | Bin 0 -> 7946 bytes .../locale/tr/LC_MESSAGES/django.po | 328 ++++++++++++++++++ .../locale/uk/LC_MESSAGES/django.mo | Bin 0 -> 577 bytes .../locale/uk/LC_MESSAGES/django.po | 324 +++++++++++++++++ .../locale/zh_CN/LC_MESSAGES/django.mo | Bin 0 -> 8383 bytes .../locale/zh_CN/LC_MESSAGES/django.po | 325 +++++++++++++++++ rest_framework/views.py | 10 +- 47 files changed, 7165 insertions(+), 7 deletions(-) create mode 100644 rest_framework/locale/ar/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/ar/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/cs/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/cs/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/da/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/da/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/de/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/de/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/en/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/en/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/en_US/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/es/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/es/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/et/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/et/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/fr/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/fr/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/hu/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/hu/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/id/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/id/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/it/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/it/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/ko_KR/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/ko_KR/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/mk/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/mk/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/nl/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/nl/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/pl/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/pl/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/pt_BR/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/ru/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/ru/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/sk/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/sk/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/sv/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/sv/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/tr/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/tr/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/uk/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/uk/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/zh_CN/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/zh_CN/LC_MESSAGES/django.po diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.mo b/rest_framework/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fe1b676c6db0f4d0f3af1253068c558259334fbb GIT binary patch literal 4875 zcmchZU2Ggz700KgKkpYM9<%jG3PZ-AI-~*rn_kmvqhd`Nc zgS){Q&;)-2z65>%o&X=d&oBnTSHb7NUxA0g55aGPyFO_cdGLFnjQ;|B1pFf?{yzer z0RIK<1=XhvBMY7czXJXQ{0#UO_!RhC@QdIF-~jj+Q2hKI{1Uk9)5-fDP~sj0CEp){ z68F!+ec*@SW8lAA_~`wHv7h%0_z>KTf=}^YXOUyzuR#U=3Cw~20VUqduH^kSaGLjJ z@J;Z+2b28Y29NUoS5Ve}<{`sa1Z&_R_)qW%c!(s0w+1MDZGhhd{{{|%k3DP{Bj67} znSTwG{QdzBfxAAN)c7?}`28uE0pA3_2L2u_fFFSp$L`N1etrZ_@%|1des*L03|IjV zg1-c1{vW`5z{j~Mygx+F6cD`n#|`Z=t8A;(q`=uksZ%M0Q^OUX2)i@!oJ>2{3M-{d<&C~BtCT!DX)OD^6NFuZ22CSW zXX;iEOn5#*Zu`=yrHxY!$amagwNWC5_WYEx+>%PQtx{>@6%VacZ8bp)1dY*}6Nb_j zuh7h6Uc=>axMFullF8J?uGPZ}GT0Ft58Vds`ChXw=$;$3)o9gn&-EGg47!afBG!gU z2|LNxtgu*-p6Dz9wZ!SO!3^Jh!jm&_Yt3o(a%%MGel z==pgyT=8mX=2*o?nU1Yq487VQcd_*$UG!>428M{af!NJq+p6W&QQsPMifV5q4D0z! zW@2I@9g0t9%swv$8CaLZgG@bB()pS+UTAjm&7dIdFk|_drw2}*JaxRwP%fPvIOfs6 zE}>qilXGaF4>R?u<+z9SmKWGzVPyD(`FwYtEI(%Z=5boPHklW# zs(2}CMr+a4)?5`eCI?^do}rRU4LE^AA0h8Dq4@WqKhh?j+gZmR$Gs*#`8=N z4;#@Htlw49C0wt|ClzfF#=MHIM{BrWlLwi(_WxM4CKC0$!gy8utVZw0OT1iF(F)Kc z(N4Uq$=DpOuQPO$UjjHj?7S=vutk zq6SAdJMJ~FmwW&GcZwih(nf?iBP@S>@vM^Htgr@SNeCUW%tcK}d`);28fK&SAW1aT zf3iqQ3A@{TVLL;u`;HPwV-1VxgyEkhTs39h9Xz0nrh?k{xry~gJB0R0q3v|Li)d5S zfntbzj#b;9cFgKlmujyT%cUMi=y{X0h$ZpTK9s;sk{hR!?PN&XF#+)jl`EpRi&A!x zR#S7Vd1{|E?(9Hl-EQ7-ne0Om{t7T+e|BI&UOel0j5K4jcVvqi!oD_NGeK zYfo;CqmHHM3ink?*X*rzr|&cItf;8Hdu&Pp`}>soE~Jfaojap?Y1uBJ?@F{HmD795 zYVGPK6s<@Qx-)S&OHO>hD?MX`}TVSc2Sb-0#{&(`U%mkzbxrF zdEM4kt5URP(wG!)F=|rlHBo@bOSqWlrTvf9Rl&6UuZhI(JVRW_)+Br98vIVji&}5` kG(mJyPBskTS^s}?w3tiy#}JzpBI=0d5}KFO!@FtYzt|Ehl>h($ literal 0 HcmV?d00001 diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.po b/rest_framework/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 000000000..a910a7c99 --- /dev/null +++ b/rest_framework/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Eyad Toma , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Arabic (http://www.transifex.com/projects/p/django-rest-framework/language/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\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 "المستخدم غير مفعل او تم حذفه." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "حدث خطأ في المخدم." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "بيانات الدخول غير صحيحة." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "لم يتم تزويد بيانات الدخول." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "ليس لديك صلاحية للقيام بهذا الإجراء." + +#: exceptions.py:93 +msgid "Not found." +msgstr "غير موجود." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "هذا الحقل مطلوب." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "لا يمكن لهذا الحقل ان يكون فارغاً null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" ليس قيمة منطقية." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "لا يمكن لهذا الحقل ان يكون فارغاً." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "تأكد ان الحقل لا يزيد عن {max_length} محرف." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "تأكد ان الحقل {min_length} محرف على الاقل." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "عليك ان تدخل بريد إلكتروني صالح." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "هذه القيمة لا تطابق النمط المطلوب." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "الرجاء إدخال رابط إلكتروني صالح." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "الرجاء إدخال رقم صحيح صالح." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "تأكد ان القيمة أقل أو تساوي {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "تأكد ان القيمة أكبر أو تساوي {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "الرجاء إدخال رقم صالح." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "تأكد ان القيمة لا تحوي أكثر من {max_digits} رقم." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "صيغة التاريخ غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "صيغة الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" ليست واحدة من الخيارات الصالحة." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "لم يتم إرسال أي ملف." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "الملف الذي تم إرساله فارغ." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "تأكد ان اسم الملف لا يحوي أكثر من {max_length} محرف (الإسم المرسل يحوي {length} محرف)." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "رقم الصفحة \"{page_number}\" غير صالح : {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "معرف العنصر \"{pk_value}\" غير صالح - العنصر غير موجود." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "قيمة غير صالحة." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "حساب المستخدم غير مفعل." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "تعذر تسجيل الدخول بالبيانات التي ادخلتها." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "يجب أن تتضمن \"اسم المستخدم\" و \"كلمة المرور\"." diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.mo b/rest_framework/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..a5e67713ae21550f7f98eca77362383d2e830151 GIT binary patch literal 8848 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 literal 0 HcmV?d00001 diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.po b/rest_framework/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 000000000..50e7034bd --- /dev/null +++ b/rest_framework/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Jirka Vejrazka , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Czech (http://www.transifex.com/projects/p/django-rest-framework/language/cs/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Chybná hlavička. Nebyly poskytnuty přihlašovací údaje." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Chybná hlavička. Přihlašovací údaje by neměly obsahovat mezery." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Chybná hlavička. Přihlašovací údaje nebyly správně zakódovány pomocí base64." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Chybné uživatelské jméno nebo heslo." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Chybná hlavička tokenu. Nebyly zadány přihlašovací údaje." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Chybná hlavička tokenu. Přihlašovací údaje by neměly obsahovat mezery." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Chybný token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Uživatelský účet je neaktivní nebo byl smazán." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Chyba na straně serveru." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Neplatný formát požadavku." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Chybné přihlašovací údaje." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Přihlašovací údaje nebyly zadány." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "K této akci nemáte oprávnění." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Nenalezeno." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Metoda \"{method}\" není povolena." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Nelze vyhovět požadavku v hlavičce Accept." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Nepodporovaný media type \"{media_type}\" v požadavku." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Pořadavek byl limitován kvůli omezení počtu požadavků za časovou periodu." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Toto pole je vyžadováno." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Toto pole nesmí být prázdné (null)." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" nelze použít jako typ boolean." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Toto pole nesmí být prázdné.." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Zkontrolujte, že toto pole není delší než {max_length} znaků." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Zkontrolujte, že toto obsahuje alespoň {min_length} znaků" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Vložte platnou e-mailovou adresu." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Hodnota v tomto poli neodpovídá požadovanému formátu." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Vložte platnou \"zkrácenou formu\" obsahující pouze malá písmena, čísla, spojovník nebo podtržítko." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Vložte platný odkaz." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Je vyžadováno číslo." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Zkontrolujte, že hodnota je menší nebo rovna {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Zkontrolujte, že hodnota je větší nebo rovna {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Řetězec je příliš dlouhý" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Je vyžadováno číslo." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Zkontrolujte, že číslo neobsahuje více než {max_digits} čislic." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Zkontrolujte, že číslo nemá více než {max_decimal_places} desetinných míst." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Zkontrolujte, že číslo neobsahuje více než {max_whole_digits} čislic před desetinnou čárkou." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Chybný formát data a času. Použijte jeden z těchto formátů: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Bylo zadáno pouze datum místo data a času." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Chybný formát data. Použijte jeden z těchto formátů: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Bylo zadáno datum a čas, místo samotného data." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Chybný formát času. Použijte jeden z těchto formátů: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" není platnou možností." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Byl očekáván seznam položek ale nalezen \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Nebyl zaslán žádný soubor." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Zaslaná data neobsahují soubor. Zkontrolujte typ kódování ve formuláři." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Nebylo možno zjistit jméno souboru." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Zaslaný soubor je prázdný." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Zajistěte, aby jméno souboru obsahovalo maximálně {max_length} znaků (teď má {length} znaků)." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Nahrajte platný obrázek. Nahraný soubor buď není obrázkem, nebo je poškozen." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Chybné čislo stránky \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Chybný primární klíč \"{pk_value}\" - objekt neexistuje." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Chybný typ. Byl přijat typ {data_type} místo hodnoty primárního klíče." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Chybný odkaz - nebyla nalezena žádní shoda." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Chybný odkaz - byla nalezena neplatná shoda." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Chybný odkaz - objekt neexistuje." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Chybný typ. Byl přijat typ {data_type} místo očekávaného odkazu." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Objekt s {slug_name}={value} neexistuje." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Chybná hodnota." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Chybná data. Byl přijat typ {datatype} místo očakávaného slovníku." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Tato položka musí být unikátní." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Položka {field_names} musí tvořit unikátní množinu." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Tato položka musí být pro datum \"{date_field}\" unikátní." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Tato položka musí být pro měsíc \"{date_field}\" unikátní." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Tato položka musí být pro rok \"{date_field}\" unikátní." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Chybné číslo verze v hlavičce Accept" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Chybné číslo verze v odkazu." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Chybné číslo verze v hostname." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Chybné čislo verze v URL parametru." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Uživatelský účet je zamčen." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Se zadanými údaji nebylo možné se přihlásit." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Musí obsahovat \"uživatelské jméno! a \"heslo\"." diff --git a/rest_framework/locale/da/LC_MESSAGES/django.mo b/rest_framework/locale/da/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..f947f90713f9a6f079c361caa166ebb0a82909d0 GIT binary patch literal 8452 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, 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Danish (http://www.transifex.com/projects/p/django-rest-framework/language/da/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: da\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Ugyldig basic header. Ingen legitimation angivet." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Ugyldig basic header. Legitimationsstrenge må ikke indeholde mellemrum." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Ugyldig basic header. Legitimationen er ikke base64 encoded på korrekt vis." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Ugyldigt brugernavn/kodeord." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Ugyldig token header." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Ugyldig token header. Token-strenge må ikke indeholde mellemrum." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Ugyldigt token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Inaktiv eller slettet bruger." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Der er sket en serverfejl." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Misdannet forespørgsel." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Ugyldig legitimation til autentificering." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Legitimation til autentificering blev ikke angivet." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Du har ikke lov til at udføre denne handling." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Ikke fundet." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Metoden \"{method}\" er ikke tilladt." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Kunne ikke efterkomme forespørgslens Accept header." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Forespørgslens media type, \"{media_type}\", er ikke understøttet." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Forespørgslen blev neddroslet." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Dette felt er påkrævet." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Dette felt må ikke være null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" er ikke en tilladt boolsk værdi." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Dette felt må ikke være tomt." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Tjek at dette felt ikke indeholder flere end {max_length} tegn." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Tjek at dette felt indeholder mindst {min_length} tegn." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Angiv en gyldig e-mailadresse." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Denne værdi passer ikke med det påkrævede mønster." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Indtast en gyldig \"slug\", bestående af bogstaver, tal, bund- og bindestreger." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Indtast en gyldig URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Et gyldigt heltal er påkrævet." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Tjek at værdien er mindre end eller lig med {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Tjek at værdien er større end eller lig med {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Strengværdien er for stor." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Et gyldigt tal er påkrævet." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Tjek at der ikke er flere end {max_digits} cifre i alt." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Tjek at der ikke er flere end {max_decimal_places} cifre efter kommaet." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Tjek at der ikke er flere end {max_whole_digits} cifre før kommaet." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datotid har et forkert format. Brug i stedet et af disse formater: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Forventede en datotid, men fik en dato." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Dato har et forkert format. Brug i stedet et af disse formater: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Forventede en dato men fik en datotid." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Klokkeslæt har forkert format. Brug i stedet et af disse formater: {format}. " + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" er ikke et gyldigt valg." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Forventede en liste, men fik noget af typen \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Ingen medsendt fil." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Det medsendte data var ikke en fil. Tjek typen af indkodning på formularen." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Filnavnet kunne ikke afgøres." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Den medsendte fil er tom." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Sørg for at filnavnet er højst {max_length} langt (det er {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Medsend et gyldigt billede. Den medsendte fil var enten ikke et billede eller billedfilen var ødelagt." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Ugyldig side \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Ugyldig primærnøgle \"{pk_value}\" - objektet findes ikke." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Ugyldig type. Forventet værdi er primærnøgle, fik {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Ugyldigt hyperlink - intet URL match." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Ugyldigt hyperlink - forkert URL match." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Ugyldigt hyperlink - objektet findes ikke." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Forkert type. Forventede en URL-streng, fik {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Object med {slug_name}={value} findes ikke." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Ugyldig værdi." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Ugyldig data. Forventede en dictionary, men fik {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Dette felt skal være unikt." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Felterne {field_names} skal udgøre et unikt sæt." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Dette felt skal være unikt for \"{date_field}\"-datoen." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Dette felt skal være unikt for \"{date_field}\"-måneden." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Dette felt skal være unikt for \"{date_field}\"-året." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Ugyldig version i \"Accept\" headeren." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Ugyldig version i URL-stien." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Ugyldig version i hostname." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Ugyldig version i forespørgselsparameteren." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Brugerkontoen er deaktiveret." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Kunne ikke logge ind med den angivne legitimation." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Skal indeholde \"username\" og \"password\"." diff --git a/rest_framework/locale/de/LC_MESSAGES/django.mo b/rest_framework/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..48245c6095528c08ac4ffc8593e2dd3fba459c65 GIT binary patch literal 6575 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\n" +"Language-Team: German (http://www.transifex.com/projects/p/django-rest-framework/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Ungültiger basic header. Keine Zugangsdaten angegeben." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Ungültiger basic header. Zugangsdaten sollen keine Leerzeichen enthalten." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Ungültiger basic header. Zugangsdaten sind nicht korrekt mit base64 kodiert." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Ungültiger Benutzername/Passwort" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Ungültiger token header. Keine Zugangsdaten angegeben." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Ungültiger token header. Zugangsdaten sollen keine Leerzeichen enthalten." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Ungültiges Token" + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Benutzer inaktiv oder gelöscht." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Ein Serverfehler ist aufgetreten." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Fehlerhafte Anfrage." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Falsche Anmeldedaten." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Anmeldedaten fehlen." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Sie sind nicht berechtigt, diese Aktion durchzuführen." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Nicht gefunden." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Methode \"{method}\" nicht erlaubt." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Kann den Accept header der Anfrage nicht erfüllen." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Nicht unterstützter Medientyp \"{media_type}\" in der Anfrage." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Die Anfrage wurde gedrosselt." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Dieses Feld ist erforderlich." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Dieses Feld darf nicht Null sein." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" ist kein gültiger Boole'scher Wert." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Dieses Feld darf nicht leer sein." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Stelle sicher, dass dieses Feld nicht mehr als {max_length} Zeichen lang ist." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Stelle sicher, dass dieses Feld mindestens {min_length} Zeichen lang ist." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Gebe eine gültige E-Mail Adresse an." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Dieser Wert passt nicht zu dem erforderlichen Muster." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Gebe ein gültiges \"slug\" aus Buchstaben, Ziffern, Unterstrichen und Minuszeichen ein." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Gebe eine gültige URL ein." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Eine gültige Ganzzahl ist erforderlich." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Stelle sicher, dass dieser Wert kleiner oder gleich {max_value} ist." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Stelle sicher, dass dieser Wert größer oder gleich {max_value} ist." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Zeichenkette zu lang." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Eine gültige Zahl ist erforderlich." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Stelle sicher, dass es insgesamt nicht mehr als {max_digits} Ziffern lang ist." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Stelle sicher, dass es nicht mehr als {max_decimal_places} Nachkommastellen lang ist." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Stelle sicher, dass es nicht mehr als {max_whole_places} Stellen vor dem Komma lang ist." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datum- und Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Erwarte eine Datum- und Zeitangabe, erhielt aber ein Datum." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Datum hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Erwarte ein Datum, erhielt aber eine Datum- und Zeitangabe." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" ist keine gültige Option." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Erwarte eine Liste von Elementen, erhielt aber den Typ \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Es wurde keine Datei übermittelt." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Die übermittelten Daten sind keine Datei. Prüfe den Kodierungstyp im Formular." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Der Dateiname konnte nicht ermittelt werden." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Die übermittelte Datei ist leer." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "Ungültiger Wert." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Ungültige Daten. Dictionary erwartet, aber {datatype} erhalten." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Dieses Feld muss eineindeutig sein." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Benutzerkonto ist gesperrt." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "\"username\" und \"password\" sind erforderlich." diff --git a/rest_framework/locale/en/LC_MESSAGES/django.mo b/rest_framework/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..746915ff11f3b04ac4d13877a632c754d53f8f8e GIT binary patch literal 8553 zcmeI1TWnlM8GujIk`$LVr8F&C_&``L?ESE zJIeUmIdl8}fByMrMtSFfyT0v_o+dv+wmUq}z&GyV59#@xp7$8kP{9}Bqwp&HD0~e{ zzdyrIz<$@Hr^+NAPj@^^!k^GR~XuD7^0;&)W;fp^SG0o`Bcjlkgol1|R*9=T+f6 zybpd0O1qcfKKLgn_SkW+=Y1AF0p*!{my=9|2~ zsQ00i*PyK5ci=JjGbr}{ds%+`!-f3M!5ZIlDC>F+9)N#?!|*|bz6>Yf$KXpPe*q6s z{yqHV9iF%ABZVDbd$7>sT_}3)+*A0~UMO;%h9cJ&VFfP3gYX)Z`QL=%S9?%e^!pMV zg;(Jv_%0MZPGJo32MeEuKY`*0|AEKhah&-aOduk9uR@vsH&E8;A0_WcDH-PoJPO-T z{OSi##(M*vfV)_1@vBiNe)VNog};InQ2dWRYc*EJ@Z4xAg9m0!j3cwfO@1b7Zlb$RKu@B9D--PlPLvt>KdR$xbpVdV~d! zP~voZ3Z5oQV!JuNcmm>b&gW6hJ52rnS<+wux9@iFTm1D5xeVFPFT}Qz*k;bZ32)uR zQfx}QDaqQ49c6FHddgaklO@S|i!VsJn=I=iN&HsQDY7C@kY#Um5-x+VYeC%3?fRe! zG8HFQX|<%oprPiIBs4m%Zr{RhC4q0G0X=ea;}Em6Q{z?djLJ;9WKw0)G)YzB`+1t0 zhExJcC{FXX|d$~ceaZ>Y;{%fvSDwGEP3`E-z?4l}iEQsdNUr^!;#kZHz} zJZ!jc8SS%$RYh+%lgX?)1WQ=(4a2hR3xQ#Y z#Ox@G*Rd{ZQ4r6CCT`l+y7F5()xI@pwq3tCx!x|f?n?_@E)&y6iJKLR-q=&^3vdy{ zTPy4KZvUB_53Gr@?yz>XZB!YY=A^Jb zxT$Z5&bQfXDn|Ph;g#Mta8$isE19jTy82=omDv;nA0%q{CRoA2Z3)Yyi2CK0)mK3tV{=GYNsP{@V0srXj?3&B;^>IOml#cFnU)|Pl7 z!f!RkGs&i~+KZ;0<^=I9NxQwp92;zgk961eV0TTO(cD`0ro|sk8V2#As;J%)h|LMS zerxN7NOy}C=jX4;f;SSpUTkPq@R#0TZQT^_63i7lWqpts9^vVHB8Yd?WCdxqSZ={V zgiLNevP~9E++E6BnsIaU8Q1yt*Ua_q37d1!N=U1z1RxkQ;V)oA4bOf=kt#)}fq~Y#}-8If40P zY%6JCXPWSXdxX1(gkiEQ=l68ZP7h)~%p0s(N5?^>WBPP7B{*Q~g&;I)nK+&0^HD$~ zqT z;+iJPR@GR`_>1m*bx}c%0T()1Tsd^b*4K~Yti)kuqPAU?%I*owkKPSz!}mz9x;4Ra z^P!FxH+9-j8RucRxlR1D0C~|3|loNQO&pl=HTxTQwR-+_qa; z<#tSO#UqJEPWX<6WJlmd+>mG9|7~wJ*7Nu-H-#{1%C>Yq*a^EEV{z4+gCwCF*xi8|5s&GeUT}^V8yC#e%+sCj0xM?^)BS=Vd zxjSfg6yO%QW!T3Loj+=?PIPvU^T9Eic#9JJ=Xv8v= z>8u$vDrfU%R+&laYT&~8nF{BsdoWbS@wd7% zM4GMJ9WUCQ)+7s5Q%VP%qq%IYd^yJ9wS=p@e$sTtW9tH(yv@cxqnUjK#Je_<9c z)&2`p-nIP~X6GF0zc7jHoTdF2W~Mq=|H9n*?YjBN*neU6Uzq(DX8(oBgYD-3 MEzy5r-t>j}KVJtP)c^nh literal 0 HcmV?d00001 diff --git a/rest_framework/locale/en/LC_MESSAGES/django.po b/rest_framework/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..f3db69e5e --- /dev/null +++ b/rest_framework/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,324 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: English (http://www.transifex.com/projects/p/django-rest-framework/language/en/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Invalid basic header. No credentials provided." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Invalid basic header. Credentials string should not contain spaces." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Invalid basic header. Credentials not correctly base64 encoded." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Invalid username/password." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Invalid token header. No credentials provided." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Invalid token header. Token string should not contain spaces." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Invalid token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "User inactive or deleted." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "A server error occurred." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Malformed request." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Incorrect authentication credentials." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Authentication credentials were not provided." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "You do not have permission to perform this action." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Not found." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Method \"{method}\" not allowed." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Could not satisfy the request Accept header." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Unsupported media type \"{media_type}\" in request." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Request was throttled." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "This field is required." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "This field may not be null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" is not a valid boolean." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "This field may not be blank." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Ensure this field has no more than {max_length} characters." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Ensure this field has at least {min_length} characters." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Enter a valid email address." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "This value does not match the required pattern." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Enter a valid \"slug\" consisting of letters, numbers, underscores or hyphens." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Enter a valid URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "\"{value}\" is not a valid UUID." + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "A valid integer is required." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Ensure this value is less than or equal to {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Ensure this value is greater than or equal to {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "String value too large." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "A valid number is required." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Ensure that there are no more than {max_digits} digits in total." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Ensure that there are no more than {max_decimal_places} decimal places." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Ensure that there are no more than {max_whole_digits} digits before the decimal point." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datetime has wrong format. Use one of these formats instead: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Expected a datetime but got a date." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Date has wrong format. Use one of these formats instead: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Expected a date but got a datetime." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Time has wrong format. Use one of these formats instead: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" is not a valid choice." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Expected a list of items but got type \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "No file was submitted." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "The submitted data was not a file. Check the encoding type on the form." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "No filename could be determined." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "The submitted file is empty." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Ensure this filename has at most {max_length} characters (it has {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Upload a valid image. The file you uploaded was either not an image or a corrupted image." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "Expected a dictionary of items but got type \"{input_type}\"." + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Invalid page \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "Invalid cursor" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Invalid pk \"{pk_value}\" - object does not exist." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Incorrect type. Expected pk value, received {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Invalid hyperlink - No URL match." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Invalid hyperlink - Incorrect URL match." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Invalid hyperlink - Object does not exist." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Incorrect type. Expected URL string, received {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Object with {slug_name}={value} does not exist." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Invalid value." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Invalid data. Expected a dictionary, but got {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "This field must be unique." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "The fields {field_names} must make a unique set." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "This field must be unique for the \"{date_field}\" date." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "This field must be unique for the \"{date_field}\" month." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "This field must be unique for the \"{date_field}\" year." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Invalid version in \"Accept\" header." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Invalid version in URL path." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Invalid version in hostname." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Invalid version in query parameter." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "User account is disabled." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Unable to log in with provided credentials." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Must include \"username\" and \"password\"." diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.mo b/rest_framework/locale/en_US/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..eb60d9d7e1123950e713bfcb32e08531629dca0f GIT binary patch literal 378 zcmYL^K~KUk7=|%=+R?Lz9=zd)8$^Q@4V4vKTsJZXiQX#IS%dA;6{A1Izvpl9TVmu* zp7hY?Yv1qZ_~^TXIY3U3Q{)giL)r|HF3FMq)>V54tBOSM{eDp|3?|liW$?SN8hd;7>Cfw})aIJ3 gU!`l5zgd=381B8c){An$&Dw6XsVEsfYaeue0Zr^?I{*Lx literal 0 HcmV?d00001 diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po index 23f76ff7f..11d94e9ca 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-30 16:23+0000\n" +"POT-Creation-Date: 2015-01-30 16:40+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -69,7 +69,7 @@ msgstr "" msgid "You do not have permission to perform this action." msgstr "" -#: exceptions.py:93 +#: exceptions.py:93 views.py:77 msgid "Not found." msgstr "" @@ -309,6 +309,10 @@ msgstr "" msgid "Invalid version in query parameter." msgstr "" +#: views.py:81 +msgid "Permission denied." +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" diff --git a/rest_framework/locale/es/LC_MESSAGES/django.mo b/rest_framework/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..814db7be9869ad30aab02fc08c8b1bec51921dc1 GIT binary patch literal 8906 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 bUU$}juqdxl3dikFi@i#n#VLk_(z^Fw@*D2A literal 0 HcmV?d00001 diff --git a/rest_framework/locale/es/LC_MESSAGES/django.po b/rest_framework/locale/es/LC_MESSAGES/django.po new file mode 100644 index 000000000..28ef893d4 --- /dev/null +++ b/rest_framework/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,327 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# José Padilla , 2015 +# Miguel González , 2015 +# Sergio Infante , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Spanish (http://www.transifex.com/projects/p/django-rest-framework/language/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Cabecera básica inválida. Las credenciales no fueron suministradas." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Cabecera básica inválida. La cadena con las credenciales no debe contener espacios." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Cabecera básica inválida. Las credenciales incorrectamente codificadas en base64." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Nombre de usuario/contraseña inválidos." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Cabecera token inválida. Las credenciales no fueron suministradas." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Cabecera token inválida. La cadena token no debe contener espacios." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Token inválido." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Usuario inactivo o borrado." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Se ha producido un error en el servidor." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Solicitud con formato incorrecto." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Credenciales de autenticación incorrectas." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Las credenciales de autenticación no se proveyeron." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Usted no tiene permiso para realizar esta acción." + +#: exceptions.py:93 +msgid "Not found." +msgstr "No encontrado." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Método \"{method}\" no permitido." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "No se ha podido satisfacer la solicitud de cabecera de Accept." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Tipo de medio \"{media_type}\" incompatible en la solicitud." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Solicitud fue regulada (throttled)." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Este campo es requerido." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Este campo no puede ser nulo." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" no es un booleano válido." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Este campo no puede estar en blanco." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Asegúrese de que este campo no tenga más de {max_length} caracteres." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Asegúrese de que este campo tenga al menos {min_length} caracteres." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Introduzca una dirección de correo electrónico válida." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Este valor no coincide con el patrón requerido." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Introduzca un \"slug\" válido consistente en letras, números, guiones o guiones bajos." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Introduzca una URL válida." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Introduzca un número entero válido." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Asegúrese de que este valor es menor o igual a {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Asegúrese de que este valor es mayor o igual a {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Cadena demasiado larga." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Se requiere un número válido." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Asegúrese de que no haya más de {max_digits} dígitos en total." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Asegúrese de que no haya más de {max_decimal_places} decimales." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Asegúrese de que no haya más de {max_whole_digits} dígitos en la parte entera." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Fecha/hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Se esperaba un fecha/hora en vez de una fecha." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Fecha con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Se esperaba una fecha en vez de una fecha/hora." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" no es una elección válida." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Se esperaba una lista de elementos en vez del tipo \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "No se envió ningún archivo." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "La información enviada no era un archivo. Compruebe el tipo de codificación del formulario." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "No se pudo determinar un nombre de archivo." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "El archivo enviado está vació." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Asegúrese de que el nombre de archivo no tenga más de {max_length} caracteres (tiene {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Adjunte una imagen válida. El archivo adjunto o bien no es una imagen o bien está dañado." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Página \"{page_number}\" inválida: {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Clave primaria \"{pk_value}\" inválida - objeto no existe." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Tipo incorrecto. Se esperaba valor de clave primaria y se recibió {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Hiperenlace inválido - No hay URL coincidentes." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Hiperenlace inválido - Coincidencia incorrecta de la URL." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Hiperenlace inválido - Objeto no existe." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Tipo incorrecto. Se esperaba una URL y se recibió {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Objeto con {slug_name}={value} no existe." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Valor inválido." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Datos inválidos. Se esperaba un diccionario pero es un {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Este campo debe ser único." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Los campos {field_names} deben formar un conjunto único." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Este campo debe ser único para el día \"{date_field}\"." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Este campo debe ser único para el mes \"{date_field}\"." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Este campo debe ser único para el año \"{date_field}\"." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Versión inválida en la cabecera \"Accept\"." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Versión inválida en la ruta de la URL." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Versión inválida en el nombre de host." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Versión inválida en el parámetro de consulta." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Cuenta de usuario está deshabilitada." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "No puede iniciar sesión con las credenciales proporcionadas." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Debe incluir \"username\" y \"password\"." diff --git a/rest_framework/locale/et/LC_MESSAGES/django.mo b/rest_framework/locale/et/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..ca9b6ec4b7616e9efccc29ae0a2b604aa476aef8 GIT binary patch literal 2018 zcma)+OK%)S5XYMk9$B7>c!Y;Su?LbU%&zT(BwAw>;W)^O4VJz3)hf@lciYp`z4T*k z3*~^gaOA=TaX~T{?zu!k0+}ztg);{}1pl60uN}#eGE&cPrn??pRo(T^`LjO?Jg;NC zkMT3c+v)omyzuOSUx54IZSYU91D-h}#7%GxJP$@N>3oI3sgqN(Vx@_T!ogTeESiyI^wJrwWP8Bg?^;*1#B%fj z!Q?s?lA1>XO=43}^GK}O5^KRJ(;EjEtR>B^CtIT1N3`2Y>LP{h5T|kyc2CS#ypOsB z+Cq7vM#k#3Iu`)c5pd1dR z35qpBT}Oz-d2XXYTps1}Nf|q;SCrvAsG&?XDWsMmt*kh^BlD0g7TJb$URl$jJ3DN8 zmR1+my0q<(Go-6*tjd9PAzSu6Rb-#Xp3l0rLyeWMy4ifHI8?JcD13)zrlvp1rlzxN zQ#5^}Gjr|g6dnj>t8%E4i__(9j^viuhpg+^c+J7O4t4vsVo&pZr+iS7J`J|IjggNh zTDh%0Yb;^Uk$W=hO0GJz;Da>^2k>$~gh8j>-re18g=AH2%e|!1MiNu#zCCCcb;KD? zOvok?xAoX$TZZWwNN_A9d;IvtKe6eFP_l#{Rr;r}b=(X{tSOJ0F~WCFmI03Ijxd>) z$jFdnHn2E4#5`^a8;l&4>|@{^4$+wC+nzQ1!v}_ODI{-^$L-MtS4_>vB}0>TFp1+? zi>C$wHC!u0S}BA(P-TgX9Uf?vCQ%kNn&`Ef%u0nD-N&ldOyTfRU)A#Cv|<%g*snwr zU=w0N6)ZT|hA-v4L@!Wirs(_i>L`MgX}QJNi$P8z8p6GCge4BD9aRh0J{FUOG>5iO T2Bn5|(NMuMZ8jh)=nnq_MD1-c literal 0 HcmV?d00001 diff --git a/rest_framework/locale/et/LC_MESSAGES/django.po b/rest_framework/locale/et/LC_MESSAGES/django.po new file mode 100644 index 000000000..dec03d4d8 --- /dev/null +++ b/rest_framework/locale/et/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Tõnis Kärdi , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Estonian (http://www.transifex.com/projects/p/django-rest-framework/language/et/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\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 "Vale kasutajatunnus/salasõna." + +#: 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 "Kasutaja on inaktiivne või kustutatud." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Väli on kohustuslik." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Väli ei tohi olla tühi." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Sisesta kehtiv e-posti aadress." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Väärtus ei ühti etteantud mustriga." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Sisesta korrektne URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Veendu, et väärtus on väiksem kui või võrdne väärtusega {max_value}. " + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Veendu, et väärtus on suurem kui või võrdne väärtusega {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Sõne on liiga pikk." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Veendu, et kokku pole rohkem kui {max_digits}." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Veendu, et komakohti pole rohkem kui {max_decimal_places}. " + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Kasutajakonto on suletud" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Sisselogimine antud tunnusega ebaõnnestus." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Peab sisaldama \"kasutajatunnust\" ja \"slasõna\"." diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.mo b/rest_framework/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..68519d4539c4e2c1b67a343c83889ea9cadc74df GIT binary patch literal 6975 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 literal 0 HcmV?d00001 diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.po b/rest_framework/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..e8597c305 --- /dev/null +++ b/rest_framework/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,326 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Etienne Desgagné , 2015 +# Martin Maillard , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: French (http://www.transifex.com/projects/p/django-rest-framework/language/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "En-tête « basic » non valide. Informations d'identification non fournies." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "En-tête « basic » non valide. Les informations d'identification ne doivent pas contenir d'espaces." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "En-tête « basic » non valide. Encodage base64 des informations d'identification incorrect." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Nom d'utilisateur et/ou mot de passe non valide(s)." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "En-tête « token » non valide. Informations d'identification non fournies." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "En-tête « token » non valide. Un token ne doit pas contenir d'espaces." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Token non valide." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Utilisateur inactif ou supprimé." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Une erreur du serveur est survenue." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Requête malformée" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Informations d'authentification incorrectes." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Informations d'authentification non fournies." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Vous n'avez pas la permission d'effectuer cette action." + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Méthode \"{method}\" non autorisée." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Ce champ est obligatoire." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Ce champ ne peut être null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" n'est pas un booléen valide." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Ce champ ne peut être vide." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Assurez-vous que ce champ comporte au plus {max_length} caractères." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Assurez-vous que ce champ comporte au moins {min_length} caractères." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Saisissez une adresse email valable." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Cette valeur ne satisfait pas le motif imposé." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Ce champ ne doit contenir que des lettres, des nombres, des tirets bas _ et des traits d'union." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Saisissez une URL valide." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Saisissez un nombre entier valide." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Assurez-vous que cette valeur est inférieure ou égale à {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Assurez-vous que cette valeur est supérieure ou égale à {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Chaîne de caractères trop longue." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Un nombre valide est requis." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Assurez-vous qu'il n'y a pas plus de {max_digits} chiffres au total." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Assurez-vous qu'il n'y a pas plus de {max_decimal_places} chiffres après la virgule." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Assurez-vous qu'il n'y a pas plus de {max_whole_digits} chiffres avant la virgule." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" n'est pas un choix valide." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Aucun fichier n'a été soumis." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "La donnée soumise n'est pas un fichier. Vérifiez le type d'encodage du formulaire." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Le nom de fichier n'a pu être déterminé." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Le fichier soumis est vide." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Assurez-vous que le nom de fichier comporte au plus {max_length} caractères (il en comporte {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Page \"{page_number}\" non valide : {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Clé primaire \"{pk_value}\" non valide - l'objet n'existe pas." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "L'object avec {slug_name}={value} n'existe pas." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Valeur non valide." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Ce champ doit être unique." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Les champs {field_names} doivent former un ensemble unique." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Ce champ doit être unique pour la date \"{date_field}\"." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Ce champ doit être unique pour le mois \"{date_field}\"." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Ce champ doit être unique pour l'année \"{date_field}\"." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Version non valide dans l'en-tête « Accept »." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Version non valide dans l'URL." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Version non valide dans le nom d'hôte." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Version non valide dans le paramètre de requête." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Ce compte est désactivé." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Impossible de se connecter avec les informations d'identification fournies." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "\"username\" et \"password\" doivent être inclus." diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.mo b/rest_framework/locale/hu/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..451b0b9ad2aa949c517f7f00a846a35397bf7bcd GIT binary patch literal 9215 zcmb`MNsJuT8OMtyKn9WkNg#wQ&jSGi?jD;xV@zz14Ypzfvd1JygwgBnH{CT|OH=iF z#@(YiBpf1gbEKds_~HXXBgIiHOIEN*F2yLyqM%%Ii4;X5;t*xw5OD~~^7~#@Z?kxY zrKFmFSJivp`}Xg9Z~k=Sl|S&f4sm~h``ODqPlKngv- z-{-+2JpTl|5quAn^*=f0dAEW06!;|g0M9A-6>u38I&XpxgJaiv-q*k>a0+|@EQ7xU z<@>wfc5vdlT<&jzck|o@<^3z*PVjf2%=;g>2P}UAUcdnSJh)gqzXS?@uYkhuuL^v- zcz*?xM4mgrFM*Tbo!~RzP2jgd1^y631n)I)0(_(R{x9%mp0E8>&ev_A=y@8H^-hEG z{k7ux_n_G8AK-)F*!8)7_kr@<0fqmcf)n6xK(XU{;631@H+bGIa1MM1JPqCgzFXk+ z2y-vbcY{B_%=5kl?&SH_&*b(v0*agoDC16nLiY_&f$xBifH!>B^NxZ~6nGl^4$prD z#cx8CBhPPu$HB)DavFRYOu%V82<}1^&Q{_Na*16@U+%)4M=sHgTvu~T-h7n%W889Gk%P0h_?qZR zt|@NuIq?tib-8e3?<($_xyA3qXG+}S19I))#`G?h#ZPcc=a*t@iAT9`C7fr`k6LZJ zG_L$qMX^;{&FjFgsY)CNMn~n1M^qcJUo~ZKpGr+KZxUsaBu-RZt+tcI)Z{}>%8#t6 z^PLqE^L*PM`53js%9?L&+c1&!tJ?Z;q^eAiM;)YU!6e4<*-GMhzb4B}#qFTx-ldFB zXFH17ZY7giwXa$=Evp(v*Gy9O4rpssLqjNuqq>@nlTh2TI*}R`N8GbAnTPyCs{AOm zOr2DV`J1Jjg!RJ@OXgsdwvoj)w3RjashwyljPuu#S`78cnVPBkp$=wRfv%c#Nfpm3 ze=d7lrsLOrEAt&9n6k0e!PW^ZH1LR-e0rZu#h@3F!VDOTG58Q!%s%TIOeAVYSlkM` zEQWqG6PT!O8%wI%(21^ElcXEXi{jOK*?cY|bYUiz4P&<|8eKb6ZTE2z*jp^V-8ISw ze%vrMi-OT{BVz*i7N&h`!n9A&c3MUid1po*md4B8;Rp#5 z7*Kk{;2$0(^UA8vGvcY#CManKkzY0bJc2C3xXxF&#MD+Zx6#HEF^?ofo%+>Y9#K<6 zIZ4uH!F|w?apuAMl|dkvo(@Nw&CeNEX``RWu({PlXxfsP7v|Ta@kBh58V4D%H(cZ& zQWA#stCCT5(%I2BhtrZX!(bhp$RvRuHC0IsEF(%LTUHzE4}_gfMm$-W6ARa3Tve22 zPU0W~Vl7=4#}Jsw-Fs=A#2ezCydti8a$z!tHH)|zhspTTlx^HJQQvl(>TPuRw41zj zBe~fFX4_Op$wLz@ou&(MLdELI;o^SikvNumhvCQbvNhi0c`#)#MwH#scE}nO;U0ko z*-lnnm-_Q{=k^sTATUE&$Ol+zFuASl z6<)L8+lE?{oH!%Xmi8<<^-+mdxzn*+1h=GZB(b%DP(1F^Ly?bd9IHSlb@Ve$>2mdn z?C;Q#r%;9>VyK%GW|FLrUTAdCj7;c@BDqq#rSj%Uge|M7hN(7PQFLiQa<|KR7*{H# z=z29BrzPrz30t-!ANysKAA`1O=zXX=&L(KC66mNoGHK{z)DD8tZz3483ZdY}aWyWP zz|1%uU~YN%0MkNJ7cM!>=a!c17>y%tIIZ~POg^x9DA~v{-_wvp1YShnI{o2Wv=fo8 z;JcLfAg)VTIv?!i+2Q0{_D;~wwOVl^6_h~ob#8HqA0CQ2Cp7vIcA^!;TFPyn+iCJ} zN0qxHt9IhHYP%s&l<+YG0PTVEGm?Z1mx8Zd?P<4U;hcs?H_Q7fffW&?DQQ0yK?nl( z#gFKBmYB55~#zvnDAWl*WRLGN~S}__ojcq-wOso(kHD4oZ(< zs&rCCE%%u2xpz0a1^KdPd!)AQQM>NiJ?8D}_L?xs?Iu*F-ci|dGM_C+9SSrq?>DtC zuWYtVcG+wKon?4t#mhz;xs#Jv-2W^Y9bJe_BMvIW-iEK9E7LWur^Ne% zj?a3s{R}$NxexCnKU!-0edO%KXjXls<_h~j(u*Y{6U ztJ&<#To7f;^J~nxAyu_~j?j*fjBt+e28|aNquD2KL5^-OiL6#n-od?p8YAlH5g|WG zOI*MjQ~gnmD)R;5XnEZJ=ve>bI2w%NjtVcg-`%BeiZ^yz2kdugS}5-t<_ZXnfmopEvq zpiI!rUJq*X(&ArY>{^$gDkAnBYmAbWUxhD4XAuwK#Vbu_DaIpp%;s;qm6xmKKr zymPkICS8i9dj5+kBH3BH6gj+W$6ZDRT}y|pU{Rp9L?C-HuWvas`IdgT0+bbAG+Z%y z+xcd8z81qM#Yyx>e#Hk(uyU&Gy;i7M74n-Fy-^M|ojs=l z6otWYz^L-vX}}ugk5ZA-figSqg8`L2p>~Kx{eV-hZqw8Vr`4Pn#|gr0e^@?kgOON5 zw)IUZxS}M=v^8CyB0kEA>onv*fDUf`bA!?GYflgK7FWv81xEeleJX!F68qR;$oa%* zJc!8sFx}KIibID{NpVpQHC^MtNXdO|<_1BqejX&hEl z8k!;wVKAChV%o@ywj%#d*kA>zEL|%+UWzC_DeYU$(6vC*onVMpByLiXdWr^5+O?t6 z3$iaE+~(jZ4_{86X@{)r=X;4hV$;?%0cWCWd{lL2-q>+hBluM4&~mzQ+=QV|+KTeJ z%K1PDg@nA!>4hOOE=Y8w2|0S-Q0XmLkUl`S=uEM$*hE~3lEdqv7d@|6anO-7=OQAg zVA5TpuwcDI5m~R-=r-d7nl|pz3`;_F-->=DPrWv~804iryC`2*or6eg9HD}y)7`L7 zMwHhYn^c-y>5F5b-R)H-swt?T3el045lyKZ` zEvkyVBnxj69c#&U-gKr_e~B? zdevp+oG8Zic(R7H$ZW#+vIoYVe uBWE#r)IQ-x&BgUNZ42jT>TGURxN>eD54VbaJO7<(=sC>8$41;Od;bT-vlYJp literal 0 HcmV?d00001 diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.po b/rest_framework/locale/hu/LC_MESSAGES/django.po new file mode 100644 index 000000000..14fb6544d --- /dev/null +++ b/rest_framework/locale/hu/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Zoltan Szalai , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Hungarian (http://www.transifex.com/projects/p/django-rest-framework/language/hu/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hu\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Érvénytelen basic fejlécmező. Nem voltak megadva azonosítók." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Érvénytelen basic fejlécmező. Az azonosító karakterlánc nem tartalmazhat szóközöket." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Érvénytelen basic fejlécmező. Az azonosítók base64 kódolása nem megfelelő." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Érvénytelen felhasználónév/jelszó." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Érvénytelen token fejlécmező. Nem voltak megadva azonosítók." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Érvénytelen token fejlécmező. A token karakterlánc nem tartalmazhat szóközöket." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Érvénytelen token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "A felhasználó nincs aktiválva vagy törölve lett." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Szerver oldali hiba történt." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Hibás kérés." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Hibás azonosítók." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Nem voltak megadva azonosítók." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Nincs jogosultsága a művelet végrehajtásához." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Nem található." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "A \"{method}\" metódus nem megengedett." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "A kérés Accept fejlécmezőjét nem lehetett kiszolgálni." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Nem támogatott média típus \"{media_type}\" a kérésben." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "A kérés korlátozva lett." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Ennek a mezőnek a megadása kötelező." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Ez a mező nem lehet null értékű." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "Az \"{input}\" nem egy érvényes logikai érték." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Ez a mező nem lehet üres." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Bizonyosodjon meg arról, hogy ez a mező legfeljebb {max_length} karakterből áll." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Bizonyosodjon meg arról, hogy ez a mező legalább {min_length} karakterből áll." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Adjon meg egy érvényes e-mail címet!" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Ez az érték nem illeszkedik a szükséges mintázatra." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Az URL barát cím csak betűket, számokat, aláhúzásokat és kötőjeleket tartalmazhat." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Adjon meg egy érvényes URL-t!" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Egy érvényes egész szám megadása szükséges." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Bizonyosodjon meg arról, hogy ez az érték legfeljebb {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Bizonyosodjon meg arról, hogy ez az érték legalább {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "A karakterlánc túl hosszú." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Egy érvényes szám megadása szükséges." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Bizonyosodjon meg arról, hogy a számjegyek száma összesen legfeljebb {max_digits}." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Bizonyosodjon meg arról, hogy a tizedes tört törtrészében levő számjegyek száma összesen legfeljebb {max_decimal_places}." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Bizonyosodjon meg arról, hogy a tizedes tört egész részében levő számjegyek száma összesen legfeljebb {max_whole_digits}." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Időt is tartalmazó dátum helyett egy időt nem tartalmazó dátum lett elküldve." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Időt nem tartalmazó dátum helyett egy időt is tartalmazó dátum lett elküldve." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Az idő formátuma hibás. Használja ezek valamelyikét helyette: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "Az \"{input}\" nem egy érvényes elem." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Elemek listája helyett \"{input_type}\" lett elküldve." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Semmilyen fájl sem került feltöltésre." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Az elküldött adat nem egy fájl volt. Ellenőrizze a kódolás típusát az űrlapon!" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "A fájlnév nem megállapítható." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "A küldött fájl üres." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Bizonyosodjon meg arról, hogy a fájlnév legfeljebb {max_length} karakterből áll (jelenlegi hossza: {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Töltsön fel egy érvényes képfájlt! A feltöltött fájl nem kép volt, vagy megsérült." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Érvénytelen oldal \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Érvénytelen pk \"{pk_value}\" - az objektum nem létezik." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Helytelen típus. pk érték helyett {data_type} lett elküldve." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Érvénytelen link - Nem illeszkedő URL." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Érvénytelen link. - Eltérő URL illeszkedés." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Érvénytelen link - Az objektum nem létezik." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Helytelen típus. URL karakterlánc helyett {data_type} lett elküldve." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Nem létezik olyan objektum, amelynél {slug_name}={value}." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Érvénytelen érték." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Érvénytelen adat. Egy dictionary helyett {datatype} lett elküldve." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Ennek a mezőnek egyedinek kell lennie." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "A {field_names} mezőnevek nem tartalmazhatnak duplikátumot." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" dátumra." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" hónapra." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" évre." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Érvénytelen verzió az \"Accept\" fejlécmezőben." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Érvénytelen verzió az URL elérési útban." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Érvénytelen verzió a hosztnévben." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Érvénytelen verzió a lekérdezési paraméterben." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "A felhasználó tiltva van." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "A megadott azonosítókkal nem lehet bejelentkezni." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Tartalmaznia kell a \"felhasználónevet\" és a \"jelszót\"." diff --git a/rest_framework/locale/id/LC_MESSAGES/django.mo b/rest_framework/locale/id/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..7fc98bdaa21e378da8d522b30e9e9ea1ca6cb2b1 GIT binary patch literal 497 zcmZut%T59@6xHZzmabjgg$s?A&OlTwVvIZz5{Lvx_jTqn6ldB=TLeGEZ}D&Z7ViLo zjVC$j>)d

    F>$$_W|LMcuG7Y9ucpI6`I5!dc32x((Ev{W{!zxgOt6Yb;@V~-MQ46 zfl2QrhN+bWE{**NCRiHl*~n!oWlxLDvDgR*#?LY9Sd^YhSyy@#P!FSP7DlWQLUbeQ zjmwZgLN>uqRayM6yWQx9(s>qJsa-C;u>#_mDWrqW%qr&Z_)>s)~5I3GaBx({*NMk<53QdA@DRRmv}d z(O}fu|58*7gO1T2HI4gKQf`kc&&ymY-GXE$twY~_jr****H!jYw5+ESlPaAFXud1I g8(x4)gK@sFGG~2b3nxIA>!WK&E!b?^VJkTJ11ofv761SM literal 0 HcmV?d00001 diff --git a/rest_framework/locale/id/LC_MESSAGES/django.po b/rest_framework/locale/id/LC_MESSAGES/django.po new file mode 100644 index 000000000..99b705467 --- /dev/null +++ b/rest_framework/locale/id/LC_MESSAGES/django.po @@ -0,0 +1,324 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Indonesian (http://www.transifex.com/projects/p/django-rest-framework/language/id/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=1; plural=0;\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 "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" diff --git a/rest_framework/locale/it/LC_MESSAGES/django.mo b/rest_framework/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..82ceb810ee45de6ce8cb4e6aa3258d2e98ffd69d GIT binary patch literal 5334 zcmbuCU5p$@5yyulhH!j@gz%AsK<5aYO}u+PhY)LxkuPVPoOCv}IU6OBuzF|ub~~P# z-c0x0*=HFEgn&mxlxGM*2#E(igoIEc@!$tuK|%t7KtYOxK)iv(3rPH`XLfJ*Y-39q zdG7D->8h@(uB!g;pYOZtrG%%$`yt-1z9mVHfd9CIKRmDBnIw;Ze*qPE=UqwiLGV#f z)*k^M0-ps%$0oQR{2ure_#5y9c>kUx`7HPxcocjEJP*DBJ_#OvD=+B59{3|r=Kl_^ zf%9)mlKVgh%D!I#KMH=k#cSXbjQW0TuWxI0s$orj9dL0yh{t1eG54DNsfbG1|=@P0wwNy-jgI}z-3VUdJ%jS{2{ms{<_5n?`_T_1NSli z6Hwy*NAL`IFGiy>Nx_eR9uz-bNeTcGIud5gaTC13sxO8gGq-^A;SV9aOmlgz&i66VPxxgZZ|9rfM;VoI`?mk>@K zi8)6ZpNCr)n#5-heS#foq#W~M&HU6q<$Eq23liSu~2R4X&L>yDW1)bl%f~_GL zaiI~PsOcNxB3_ss!yE@brIusgu!bX&u;VOg#kpK5Y1$B<|jR|oXr#cBE5);wfcz+`~j%7zqbdRfM-skmT zUZt+|)(1<5kx{t`;@W=IR z{?f=0wv7EVN$h?d)F2kI3?)ggTN5*PcHPXH9J|G68%*IR3d7}*QEjidAcL#(-DI`I zg9-zbzG=bADndm=3GSjP$91SqxtX%wspcCmUys5*R1#7@oK!r44w_Wdaeb185_DJ_b|!ITL@m*I60|3iz6{V~tomb9IdHG#5a zn%5bj8SgN!bcxJ(AEJb(8#Xs;i|qGxzpx|&;_b9pMY>1_>q`l7*c3*yAkc-#ly0(4 zbxlMYZDgq_M&Yu2oD>-U*=ev-^M$?~ZD)smuFH!vLQ@~hI?rdniH}Itr6nlchNQOTEQT&*$}y&G!2jU;~F%e8@(jO0`)VbqDV<3c-Kx#vmR&T7^< zPZcuPQ^%gy<-n=4$3MTWHY(19B)K?u)`K%Oiu>(9T?3^{5R<^kDx+j};Yc#~pdZkOB>)@)MS|7TC zbXgfz(nXB=ba2H{-n5z4oW=ti=)LXI^3a+gwDj_MUo?GT(n_v-`vU7lgD>LwA4K?w?esb6L;3>06i7V zrLf2~WXr>`gL@N>oLfK9IkdA*w%;&S=Qyn*qjr1hP~V1$Mn0dOgFjq4OQ86Xo-vy^)zi$ITDr9jJwzjq;u9ZOiLqr|8itnIAZ0$+wmqH8m2Cz2!*+ z(6%heM3#i>(i+WMcJw`FE4yS+`)7s8mvb-7GfeJtx6 zr8c3U(#But0%o%&`zfT|1whvPUrmu^-IlgilT;*|)9zz18XN4b4cg7gxR>~?bd&Z% zKZuMC)4R#j@fT4{-G-V;I<;|9(`sa%7E;0pZy~js3-hI+Nz|mL?kLXOW@Y+NiDrTP z+<~)gbDj;gvkgm(trJERyW$RAi~S+mW|Q+xpP#ZeIotMY$>m7nbf6Ey-yGCnhQHBe zpytx>f=iAW2t&)@uxGw>lbz=k;~5ecZyV|6ausY^L7k`4U1`;1WsZ%Ai`S9viG8E` z+E0}<{@4>5YKLHRJp_Kh#sQ*HVyu>Q+pL`|TZw(b_4~OUu(Yzwoe^?6ZXvrj!2<7D zBnJ^?+XdelTOkZ@w~|c4&{_%6OGSR_7UG&2$ZvLK^D*lqhP$t__J-p)<2^y, 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Italian (http://www.transifex.com/projects/p/django-rest-framework/language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\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 "Nome utente/password non validi" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Header del token non valido. Credenziali non fornite." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Header del token non valido. Il contenuto del token non dovrebbe contenere spazi." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Token invalido." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Utente inattivo o eliminato." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Errore del server." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Richiesta malformata." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Credenziali di autenticazione incorrette." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Non sono state immesse le credenziali di autenticazione." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Non hai l'autorizzazione per eseguire questa azione." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Non trovato." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Metodo \"{method}\" non consentito" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Impossibile soddisfare l'header \"Accept\" presente nella richiesta." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Tipo di media \"{media_type}\"non supportato." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Campo obbligatorio." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Il campo non puà essere nullo." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" non è un valido valore booleano." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Questo campo non può essere omesso." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Assicurati che questo campo non abbia più di {max_length} caratteri." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Assicurati che questo campo abbia almeno {max_length} caratteri." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Inserisci un indirizzo email valido." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Immetti uno \"slug\" valido che consista di lettere, numeri, underscore o trattini." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Inserisci un URL valido" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "È richiesto un numero intero valido." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Assicurati che il valore sia minore o uguale a {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Assicurati che il valore sia maggiore o uguale a {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "È richiesto un numero valido." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Assicurati che non ci siano più di {max_digits} cifre in totale." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Assicurati che non ci siano più di {max_decimal_places} cifre decimali." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Assicurati che non ci siano più di {max_whole_digits} cifre prima del separatore decimale." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "L'oggetto di tipo datetime è in un formato errato. Usa uno dei seguenti formati: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Atteso un oggetto di tipo datetime ma l'oggetto ricevuto è di tipo date." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "La data è in un formato errato. Usa uno dei seguenti formati: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Atteso un oggetto di tipo date ma l'oggetto ricevuto è di tipo datetime." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" non è una scelta valida." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Attesa una lista di oggetti ma l'oggetto ricevuto è di tipo \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Non è stato inviato alcun file." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Il nome del file non può essere determinato." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Il file inviato è vuoto." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "Valore non valido." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Questo campo deve essere unico." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "L'account dell'utente è disabilitato" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Impossibile eseguire il log in con le credenziali immesse." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Deve includere \"nome utente\" e \"password\"." diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..457bb53c2dd4b75e2b03631039ab60c71f64e3e1 GIT binary patch literal 8555 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 literal 0 HcmV?d00001 diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po new file mode 100644 index 000000000..963fe89a5 --- /dev/null +++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# SUN CHOI , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/django-rest-framework/language/ko_KR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko_KR\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials) 문자열은 빈칸(spaces)을 포함하지 않아야 합니다." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 base64로 적절히 부호화(encode)되지 않았습니다." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "아이디/비밀번호가 유효하지 않습니다." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "토큰 헤더가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 빈칸(spaces)를 포함하지 않아야 합니다." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "토큰이 유효하지 않습니다." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "계정이 중지되었거나 삭제되었습니다." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "서버 장애가 발생했습니다." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "잘못된 요청입니다." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "자격 인증데이터(authentication credentials)가 정확하지 않습니다." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "이 작업을 " + +#: exceptions.py:93 +msgid "Not found." +msgstr "찾을 수 없습니다." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "메소드(Method) \"{method}\"는 허용되지 않습니다." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "요청된 \"{media_type}\"가 지원되지 않는 미디어 형태입니다." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "요청이 지연(throttled)되었습니다." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "이 항목을 채워주십시오." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\"이 유효하지 않은 부울(boolean)입니다." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "이 칸이 글자 수가 {max_length} 이하인지 확인하십시오." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "이 칸이 글자 수가 적어도 {min_length} 이상인지 확인하십시오." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "유효한 이메일 주소를 입력하십시오." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "형식에 맞지 않는 값입니다." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하십시오." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "유효한 URL을 입력하십시오." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "유효한 정수(integer)를 넣어주세요." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "이 값이 {max_value}보다 작거나 같은지 확인하십시오." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "이 값이 {min_value}보다 크거나 같은지 확인하십시오." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "문자열 값이 너무 큽니다." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "유효한 숫자를 넣어주세요." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "전체 숫자(digits)가 {max_digits} 이하인지 확인하십시오." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "소수점 자릿수가 {max_decimal_places} 이하인지 확인하십시오." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "소수점 자리 앞에 숫자(digits)가 {max_whole_digits} 이하인지 확인하십시오." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datetime의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "예상된 datatime 대신 date를 받았습니다." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Date의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "예상된 date 대신 datetime을 받았습니다." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Time의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\"이 유효하지 않은 선택(choice)입니다." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "아이템 리스트가 예상되었으나 \"{input_type}\"를 받았습니다." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "파일이 제출되지 않았습니다." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "제출된 데이터는 파일이 아닙니다. 제출된 서식의 인코딩 형식을 확인하세요." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "파일명을 알 수 없습니다." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "제출된 파일이 비어있습니다." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "이 파일명의 글자수가 최대 {max_length}를 넘지 않는지 확인하십시오. (이것은 {length}가 있습니다)." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "유효한 이미지 파일을 업로드 하십시오. 업로드 하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "유효하지 않은 page \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "유효하지 않은 pk \"{pk_value}\" - 객체가 존재하지 않습니다." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "잘못된 형식입니다. pk 값 대신 {data_type}를 받았습니다." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "유효하지 않은 하이퍼링크 - 일치하는 URL이 없습니다." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "유효하지 않은 하이퍼링크 - URL이 일치하지 않습니다." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "유효하지 않은 하이퍼링크 - 객체가 존재하지 않습니다." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "잘못된 형식입니다. URL 문자열을 예상했으나 {data_type}을 받았습니다." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "{slug_name}={value} 객체가 존재하지 않습니다." + +#: relations.py:296 +msgid "Invalid value." +msgstr "값이 유효하지 않습니다." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "유효하지 않은 데이터. 딕셔너리(dictionary)대신 {datatype}를 받았습니다." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "사용자 계정을 사용할 수 없습니다." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "제공된 인증데이터(credentials)로는 로그인할 수 없습니다." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "\"아이디\"와 \"비밀번호\"를 포함해야 합니다." diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.mo b/rest_framework/locale/mk/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fc58662692761e907d5ab84799e84626140826fd GIT binary patch literal 10731 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, 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Macedonian (http://www.transifex.com/projects/p/django-rest-framework/language/mk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: mk\n" +"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Невалиден основен header. Не се внесени податоци за автентикација." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Невалиден основен header. Автентикационата низа не треба да содржи празни места." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Невалиден основен header. Податоците за автентикација не се енкодирани со base64." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Невалидно корисничко име/лозинка." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Невалиден токен header. Не се внесени податоци за најава." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Невалиден токен во header. Токенот не треба да содржи празни места." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Невалиден токен." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Корисникот е деактивиран или избришан." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Настана серверска грешка." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Неправилен request." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Неточни податоци за најава." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Не се внесени податоци за најава." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Немате дозвола да го сторите ова." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Не е пронајдено ништо." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Методата \"{method}\" не е дозволена." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Не може да се исполни барањето на Accept header-от." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Media типот „{media_type}“ не е поддржан." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Request-от е забранет заради ограничувања." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Ова поле е задолжително." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Ова поле не смее да биде недефинирано." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" не е валиден boolean." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Ова поле не смее да биде празно." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Ова поле не смее да има повеќе од {max_length} знаци." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Ова поле мора да има барем {min_length} знаци." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Внесете валидна email адреса." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Ова поле не е по правилната шема/барање." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Внесете валидно име што содржи букви, бројки, долни црти или црти." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Внесете валиден URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Задолжителен е валиден цел број." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Вредноста треба да биде помала или еднаква на {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Вредноста треба да биде поголема или еднаква на {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Вредноста е преголема." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Задолжителен е валиден број." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Не смее да има повеќе од {max_digits} цифри вкупно." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Не смее да има повеќе од {max_decimal_places} децимални места." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Не смее да има повеќе од {max_whole_digits} цифри пред децималната точка." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Датата и времето се со погрешен формат. Користете го овој формат: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Очекувано беше дата и време, а внесено беше само дата." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Датата е со погрешен формат. Користете го овој формат: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Очекувана беше дата, а внесени беа и дата и време." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Времето е со погрешен формат. Користете го овој формат: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "„{input}“ не е валиден избор." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Очекувана беше листа, а внесено беше „{input_type}“." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Ниеден фајл не е качен (upload-иран)." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Испратените податоци не се фајл. Проверете го encoding-от на формата." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Не може да се открие име на фајлот." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Качениот (upload-иран) фајл е празен." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Името на фајлот треба да има највеќе {max_length} знаци (а има {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Качете (upload-ирајте) валидна слика. Фајлот што го качивте не е валидна слика или е расипан." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Невалидна страна „{page_number}“: {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Невалиден pk „{pk_value}“ - објектот не постои." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Неточен тип. Очекувано беше pk, а внесено {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Невалиден хиперлинк - не е внесен URL." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Невалиден хиперлинк - внесен е неправилен URL." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Невалиден хиперлинк - Објектот не постои." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Неточен тип. Очекувано беше URL, a внесено {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Објектот со {slug_name}={value} не постои." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Невалидна вредност." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Невалидни податоци. Очекуван беше dictionary, а внесен {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Ова поле мора да биде уникатно." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Полињата {field_names} заедно мора да формираат уникатен збир." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Ова поле мора да биде уникатно за „{date_field}“ датата." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Ова поле мора да биде уникатно за „{date_field}“ месецот." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Ова поле мора да биде уникатно за „{date_field}“ годината." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Невалидна верзија во „Accept“ header-от." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Невалидна верзија во URL патеката." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Невалидна верзија во hostname-от." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Невалидна верзија во query параметарот." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Сметката на корисникот е деактивирана." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Не може да се најавите со податоците за најава." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Мора да се внесе „username“ и „password“." diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.mo b/rest_framework/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..b0e1ad77f0b8bf6b03341992ca76fcef6e3eb52f GIT binary patch literal 499 zcmZut%TB{E5G;aIj+{9di9;)JowNclkW!N2ziFQKprEnkTrV9AH2N7wbtx0H+m%^pp%%s31g*JJ{hjK zN;FxFUP7`oJQtNVAI?G)+5nx|M8@=~Oe_txPn`L4NC!r6z^8P`LHMNOde_wTXxAm) zjo;~Bx(F1~h4_@U#s9k7t!~OK(9m#YGY;DLBusVAEg7UnT9AU=gU%n3(XP;rbpGH> zapsaIf`)?Un4yFyCCwC|@ENPBstt8ZxfJVK&x&ns%L*26;!-2fcORE!yM9b%tmVum zv(e#$yteBMv;v`U(ysy|gIEJAGA>o0L@75`K=2xlY44z`?U%yPk&1LIRpOK0yM+DR jK2e!7&q~8HI@Tt)SSU6}8+7_)yA7I(oCm~f^_|8a9wC*1 literal 0 HcmV?d00001 diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.po b/rest_framework/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 000000000..a12155124 --- /dev/null +++ b/rest_framework/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,324 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Dutch (http://www.transifex.com/projects/p/django-rest-framework/language/nl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\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 "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.mo b/rest_framework/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..9db72cfb3535780580d2702aae172af4eed6c5d8 GIT binary patch literal 8981 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, 2015 +# Maciek Olko , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Polish (http://www.transifex.com/projects/p/django-rest-framework/language/pl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Niepoprawny podstawowy nagłówek. Brak danych uwierzytelniających." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Niepoprawny podstawowy nagłówek. Ciąg znaków danych uwierzytelniających nie powinien zawierać spacji." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Niepoprawny podstawowy nagłówek. Niewłaściwe kodowanie base64 danych uwierzytelniających." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Niepoprawna nazwa użytkownika lub hasło." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Niepoprawny nagłówek tokena. Brak danych uwierzytelniających." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Niepoprawny nagłówek tokena. Token nie może zawierać odstępów." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Niepoprawny token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Użytkownik nieaktywny lub usunięty." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Wystąpił błąd serwera." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Zniekształcone żądanie." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Błędne dane uwierzytelniające." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Nie podano danych uwierzytelniających." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Nie masz uprawnień, by wykonać tę czynność." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Nie znaleziono." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Niedozwolona metoda \"{method}\"." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Nie można zaspokoić nagłówka Accept żądania." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Brak wsparcia dla żądanego typu danych \"{media_type}\"." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Żądanie zostało zdławione." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "To pole jest wymagane." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Pole nie może mieć wartości null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" nie jest poprawną wartością logiczną." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "To pole nie może być puste." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Upewnij się, że to pole ma nie więcej niż {max_length} znaków." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Upewnij się, że pole ma co najmniej {min_length} znaków." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Podaj poprawny adres e-mail." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Ta wartość nie pasuje do wymaganego wzorca." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Wprowadź poprawną wartość pola typu \"slug\", składającą się ze znaków łacińskich, cyfr, podkreślenia lub myślnika." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Wprowadź poprawny adres URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Wymagana poprawna liczba całkowita." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Upewnij się, że ta wartość jest mniejsza lub równa {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Upewnij się, że ta wartość jest większa lub równa {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Za długi ciąg znaków." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Wymagana poprawna liczba." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Upewnij się, że liczba ma nie więcej niż {max_digits} cyfr." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Upewnij się, że liczba ma nie więcej niż {max_decimal_places} cyfr dziesiętnych." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Upewnij się, że liczba ma nie więcej niż {max_whole_digits} cyfr całkowitych." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Wartość daty z czasem ma zły format. Użyj jednego z dostępnych formatów: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Oczekiwano datę z czasem, otrzymano tylko datę." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Data ma zły format. Użyj jednego z tych formatów: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Oczekiwano daty a otrzymano datę z czasem." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Błędny format czasu. Użyj jednego z dostępnych formatów: {format}" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" nie jest poprawnym wyborem." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Oczekiwano listy elementów, a otrzymano dane typu \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Nie przesłano pliku." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Przesłane dane nie były plikiem. Sprawdź typ kodowania formatki." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Nie można określić nazwy pliku." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Przesłany plik jest pusty." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Upewnij się, że nazwa pliku ma długość co najwyżej {max_length} znaków (aktualnie ma {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Prześlij poprawny plik graficzny. Przesłany plik albo nie jest grafiką lub jest uszkodzony." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Niepoprawna strona \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Błędny klucz główny \"{pk_value}\" - obiekt nie istnieje." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Błędny typ danych. Oczekiwano wartość klucza głównego, otrzymano {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Błędny hyperlink - nie znaleziono pasującego adresu URL." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Błędny hyperlink - błędne dopasowanie adresu URL." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Błędny hyperlink - obiekt nie istnieje." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Błędny typ danych. Oczekiwano adresu URL, otrzymano {data_type}" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Obiekt z polem {slug_name}={value} nie istnieje" + +#: relations.py:296 +msgid "Invalid value." +msgstr "Niepoprawna wartość." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Niepoprawne dane. Oczekiwano słownika, otrzymano {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Wartość dla tego pola musi być unikalna." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Pola {field_names} muszą tworzyć unikalny zestaw." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "To pole musi mieć unikalną wartość dla jednej daty z pola \"{date_field}\"." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "To pole musi mieć unikalną wartość dla konkretnego miesiąca z pola \"{date_field}\"." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "To pole musi mieć unikalną wartość dla konkretnego roku z pola \"{date_field}\"." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Błędna wersja w nagłówku \"Accept\"." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Błędna wersja w ścieżce URL." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Błędna wersja w nazwie hosta." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Błędna wersja w parametrach zapytania." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Konto użytkownika jest nieaktywne." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Podane dane uwierzytelniające nie pozwalają na zalogowanie." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Musi zawierać \"username\" i \"password\"." diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..49f019294e3fc9e303cb056b1cc156ba926e54fb GIT binary patch literal 8729 zcmbuETZ~;*8OJvlxeO{d6%;S414s)!XW9a#&d}0!I+aS>Y069$gHG1jXU#dgvoD8z zndx+#MD&Fh3c)!HN2VXQAkVkot-~%!G`_|g$oPDM<(;_?B z{mED?RTpI0Jqb{2sU;d<{GW-gcGe-3P9K6X46>L*Sd>I5=^&`@RSs z<@qJ>2JjtF&i~+u=iLh4RpK;wFV7ixJNP^(bp8PD0Y|R!ya&KZa1#7FSOD=Eo!nml@8tO`DDR&IcYwbKW#50nyLzrDW z-wFQq63_b*DEsdGkh8}rQ26=^_-XJv;3)ViD1Pu7DCgbsVb41Rei?iSe5S-VLGh;v zl=DUKYoI*;1(bESA?%}I2Rs1&4x9q7#c5B1vmi^oAAsT?KLtgee}H1gn-FF{I16qA zp9Jp*Uj@aF{sT^fdr*?#6QI2RGbnuDh;qeWPJ!YtkAp(z1+WJG2^76viV`NkUEt@y z7(~V1DZQuzD0UNS@93K#4g_GN)UI7e05X>nTnHK zX|<@sps5;35*i)XH(uellfXB1?|{lox@b~m(lkj`;`>FKnx=ekq=Gm%ExvO?Y91?s zp^tG9HO~81ExfJfi!hq5n27h&x z?5nFj&xoh8JVi;{iG1G#iwLp;nKTUI1yxf6%ZQT6mVSHvg|Ksh71NE=V&P_jtBTUh860FltfO1v z7y@(7y;ny`ybpW(%sL zrDFBuuyJ2`B#x!tVfay3wnlqA54H@}h_X96uULa3+#}E?+sUcxQh%&4 z=`wp$7LXC@-Z33Yc_U_|KvOn(W%72?M9(bY4;lp99fV=BBt`dFL1+iD9~Mn)){}8m z>6kq|Nr`K8Js*TdEs zM4fzDKK9Ede+=5D%KJz!TbrP{MyTV3p-q*KaS?{Y-$XEWl|sRd&mU8QIJ53($sGK`;>T*)3 z!Y+ZLgpVNrXb-HPktAff6nt%KPtlQs9Sx6emiIn^6%(WxX+IM|2m<>hi0QSckg~gJ z=qTjNBOKREEcLeL8wDT=GMi42pC@rzR1G=VouJ_pX(IW&cBEN5I&@mctwf!i zdU!_7OMW5aEsUHrqM6#UtQ9nC2a8r#n@Psi$cgEh8imw$`L#p%+ql}bW9PlK9Xo4x z?@&8;kMFwst{q$~terHAfpD|_y8GO^qjZ+FnN-JFsPiNpN1I86>L%N%v`|LfpC{3N zZohm{_mgPf$WeGMP-kt%=xAJ>fY+i$8Z>I_!Bn3O!fk46JI_1gV`EE8OZ8k12tmdm@}TvXddMW(P$m5a_d!in~9FOqviAxe35>qa00RfTwU$Wm)BO zOox|c-E(Qck^@;8h<>2O*GbofD3j^2`<#ekdBl5lB#D>3K~vLH9hYzONuW9jv|d~^ zp>p%1K5LtBn^CgB(I7H>-7wwfbl6VHnX>MQUhy*|=rJcX)s_S|QX+aYVf=CHh#WM~ zVgRm=lbaAx%$V?f@w6sQng{bzs%khds*s{(x@Wo(`A5u-W9Fh zY}tL0Arpz?q&Ho@8!~Gq%pNg~gTNVCh%iY?7Aa}#k|y>Hwu(A%U}F4nAar%$Eay#W zt}jH!>TfNr>KJ(;T9Q?nLlIy;@Y~kc8HlJ7F`B|i)*Stzv`B5R+MgwN!@3)KsSu*X4-X^M3KRa$F?}j0~%(mRZ$B zI)BkOF;=v-L0PscfZ`-0Q!eo*=BG!J$+f@g_#hpy)WsiW zm$u0_4I>$-d|Aem{0gMY;8K~*5d#vL-KsnMcz|_ep(F|E<6@*~YBI-Qu_Y~Iu9cTL zH%7?n(RqL)t{ryW@VHUFqN|osIF}?fBinl~lDeuMT}~t@%#E%h>Cni#*=A?&&cX&; zCkM%{(LKlDTS;=Xw|4k-{7e18tx=5huD$8E$1)87!Ap#7O0R6qv8-RAY~!wJ=tb!-9$KyU7W>E258iGa; zbjl zB;1#B*!1VA_5yncW_D!l{tXQ5hVH?^bgs@O=r*%?z4GmpZBwc*Xh$Wi`>&)R`2(Nn zjn{Qg8rOAJGKEs-Q2<8WV9{X0UQ0vEX>8sOquFVFbhxy_&L|M1mYma zy3gkc@2Z2gFhmz*TkzcWLDP*iXz9HBGKsk#4vgJ}Rmv#2Y}9IniTiM`#+2p>?6ArY zcyikDvJmN%-{BWa$LjDym9%Cs>IQJ@xyOZze%fQN%;9(oF1zj+ZjbBxs1sB&XBaT_ zTZ+z1O`0G#iX&oF&@S++DnA?G+0(RgQn}Yo;^HF_xgl^>hkjUQS6_IN6fMO>268JS zy@!^*PTD=fxmR)V+~MAc_CRtglJ_!L*)7`5Q4yBy0i7Cj z97vQ@EH(7a>Zh-2NtH+~GF0{tuVHW(5zB7I28vk8ijH%HWlg@bgUfN4Oeg ztv~2*wt?=c`$Bf!hN+D1()C!wT$RGO;$nrdr7WXyL($l!qnoMqt6qaY!|xr^soCD# zHq&M1trep5Uf(aUC4Yj@&R;PCaViFA+{nwz6a-5C)5RD=$Ph%EdnE(I) literal 0 HcmV?d00001 diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..3f272f714 --- /dev/null +++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,326 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Craig Blaszczyk , 2015 +# Filipe Rinaldi , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-rest-framework/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Cabeçalho básico inválido. Credenciais não fornecidas." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Cabeçalho básico inválido. String de credenciais não deve incluir espaços." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Cabeçalho básico inválido. Credenciais codificadas em base64 incorretamente." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Usário ou senha inválido." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Cabeçalho de token inválido. Credenciais não fornecidas." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Cabeçalho de token inválido. String de token não deve incluir espaços." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Token inválido." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Usuário inativo ou removido." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Ocorreu um erro de servidor." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Pedido malformado." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Credenciais de autenticação incorretas." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "As credenciais de autenticação não foram fornecidas." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Você não tem persmissao para executar essa ação." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Não encontrado." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Método \"{method}\" não é permitido." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Não foi possível satisfazer a requisição do cabeçalho Accept." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Media type \"{media_type}\" no pedido não é suportado." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Pedido foi limitado." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Este campo é obrigatório." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Este campo não pode ser nulo." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" não é um valor boleano válido." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Este campo não pode ser em branco." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Certifique-se de que este campo não tenha mais de {max_length} caracteres." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Certifique-se de que este campo tenha mais de {min_length} caracteres." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Insira um endereço de email válido." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Este valor não corresponde ao padrão exigido." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados ou hífens." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Entrar um URL válido." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Um número inteiro válido é exigido." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Certifique-se de que este valor seja inferior ou igual a {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Certifque-se de que este valor seja maior ou igual a {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Valor da string é muito grande." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Um número válido é necessário." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Certifique-se de que não haja mais de {max_digits} dígitos no total." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Certifique-se de que não haja mais de {max_decimal_places} casas decimais." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Certifique-se de que não haja mais de {max_whole_digits} dígitos antes do ponto decimal." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Formato inválido para data e hora. Use um dos formatos a seguir: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Data e hora são necessários mas apenas data foi encontrada." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Formato inválido para data. Use um dos formatos a seguir: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Necessário uma data mas recebeu uma data e hora." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Tempo tem formato errado. Usa um desses em vez disso: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" não é um escolha válido." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Ficheiro não foi submetido." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Os dados submetidos nao foram um ficheiro. Certifique-se do tipo de codificação no formulário." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Nome do arquivo não pode ser determinado." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "O arquivo submetido ésta vázio." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Certifique-se de que o nome do ficheiro tem menos de {max_length} caracteres (tem {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Fazer upload de um imagem válido. O arquivo mandou não foi um imagem ou foi corrupto." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Página inválido \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Pk inválido \"{pk_value}\" - objeto não existe." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Tipo incorreto. Necessário valor pk, recebeu {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Hyperlink inválido - URL não combinou." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Hyperlink inválido - URL combinou errado." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Hyperlink inválido - objeto não existe." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Tipo incorreto. Necessário string URL, recebeu {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Objeto com {slug_name}={value} não existe." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Valor inválido." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Data inválido. Necessário um dicionário mas recebeu {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Esse campo deve ser unico." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Os campos {field_names} devem criar um set unico." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "O campo deve ser unico pela data \"{date_field}\"." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "O campo deve ser unico pelo anô \"{date_field}\"." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "O campo deve ser unico pela mês \"{date_field}\"." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Versão inválido no cabeçalho \"Accept\"." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Versão inválido no caminho de URL." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Versão inválido no hostname." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Versão inválida no parâmetro de query." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Conta de usário desabilitada." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Impossível fazer login com as credenciais fornecidas." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Obrigatório incluir \"usuário\" e \"senha\"." diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.mo b/rest_framework/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..d1555f1fbccfba1f8931089d25d9481707249c12 GIT binary patch literal 9778 zcmcJUZHygPdB+DxLb^>0rM$Gz!eLv24Ze4G?YyqDNx-qGMT}juP83>U&E7k^cj9|z zE;Dnz-Y#zJ1maK!EQJP;S_qIz?U#zhcQ9s8m(` z{m+^EvUjg{H+4JC?EjvbbDs0OKhK%`^i8+C=y5&7{X5)mey!&{0p4*7f4G{rdfvOi zPk;veJosDSB~bIf555EZ8CU{uyUp`u)1by%T_*dYEz@LK;gL_`@d5?h^crW-p za4-0}+dc2Y;6vafcplXIe*hl^{~LT0_~Bo1`+pq#J;sX#z69RQ`0v5@fd2`K&bvlD z?>_Kjpk)0#xDWg%unfNKSKa#q;10%5fs*@g!1saQEuQ}p9B2H_JKVa%;1uIO1%Dk} z1>X*SuNeO;D1Ltlz7xFt*F5jN;CsQhf`>r^&VsPwodd_f7mD}a0pG^>C*X&`{{W@; z``Jv-9|JXxK-u9usP%shO8-9w-w(e14W2gv9sp(EQ{ZodKPd1Q;BLmZz0vb-#Ru*O zA7}g!%>M!SMQ{xKXHfck<8L~<90B(;eh&O2@E^ez_`WxL-k*S91sA|~yv6gT!56_4 z{25pQKaLWPgWmyTa1^2B7cUg}Z{QagKaA7KEh zKL*c&-vB=iPN1Zxz{_9=-iz{{1iuD~k2`+X^A3S^@Gy84lzbx??*w=RJPEFXi{PGj zdEW1X-vG7lhoJmr)Nq4`z+GSyychg3cqjNH@B#3@Kvd-2kFz}pegu@AJ_}0U*TE;i ze+PGh2O%o|Nx^%-zW~eN55UL4*ANU(flq){@F$@3`5;Vw8+;LzJs&4H9tOV(9s&Oa zd=2;pZcOaq=9b?{P}l3YQHAw&jbF?C8{GG6z$Koop-NFs4-AlUDbr(0O#rmf7sO$CI@_pqi+{nfsVB-!q zwHN8Z+D8MUKYf?%NVwU!7%!g7Hi}bSB?sNVvMXltaBXWFOkywjBkpGHP!@E{i3!*Hn_*oc7rosXZ{d#KVgCwwgHk0^dSk*TB;#R$C zpQX%CXBP~s?M^|OnGaMdK{GS8z^?{L*?Yv#0#oxLl*G}TnT?Z%pOwwAG%#_*J*&kG z+>6wNQJS%I(k!|sryYqbY}_oF{ZZONmaOJy+LNEY70ona_c$_(4gb{fYETIqe*Ji} z?pK2Jv?<2MjmzG)>4bA(ru7yP%w%!q*SAk#zJ^B}cgwqEW&-pgDS{qjF$V7=i`i$x z024{=2#Z@`m&HaH9j^z`Tvj`6Dm6dxD_M}FH(D3PgL>I|Ei?SWOl;eT?XGBa<4Ut5 z#6@6lv3R$0Js@hU$ZCPvTjv}2nb{ZbiH3=)aOY1?J4qFF9y>+79 ziNMjc-kKXVl{iYnGz;-oyt5u;QqwNu;)Wls2**wM25{Xp9oExig__V zjK)Xeb*ZtJ5j)c*f1i>tY*i ztHo8MY}g-^XIq==^>|(-?@)`t3ZqKBRYhZ+9Y&2Gv8J;RQH7+lVLdSOq_(s*(+Ei( zEH570>{zi*H>31Tav&+Y2^kfT%u@CWo6Uz=%`7S-9oMqcQ;U{)>cD_8k2(?TmRT){ zv#c(PAGN_*WWy|uP2ErC$SBhkjxJ|PJicA&IuY?+Hv4Kp<%F#;Hl~yaZ3>|?P}z~{ z2I*L#Nxng&nJwsLw+8W}*MsyuZ}MUD)gPg>K!$G%d(%QG|_7{|ysR+S5J%e3qiD9V`v+<{VM%@i3sUB$<@d9c;g#*T(ZXT|dh zZWiIPDJdwGAh?8m6Gqg1vb9!M1FDn4*bTXIod*QdW{{m zENf1Vjm^)`mopI!XMZ6=jqrJ8*OM`6c*z|$YeZ6_wel|!-`&Hry$4PVFOqg{! zpEOBpMs4XE5;v~7&o8%q+7zA@tv%hV9oBGdq=$9X(qpxFPrv-{9^tJf0x@& zx6igOnEYzK);`ldlP~92+fV1$+UN6ClYg`QO#TXwF1q1m)-JWr@!(p%!r#^YGtjG{ zJA_dF5}U6<_A-n-$F57p{#ePs32#fLeHLENz_tD^mA(AymRWJwk)i!8JiWrIbNLF> z#rQS;tsyT|*Rei){kGP3B? zcKxJXS4{g;mf|WleA;%hWs`r2HP=OsS(jnp3L7KC8l0@;*Np68+Mng&O8e96 z2x$BMv!9h{Tf5qZ`KEX7J9g$T<0H%BSj?mB=S}+@Ki5otkzXBW=$OYzi;pc2KSnxV zQ9!U<|6P)9*+Q;_7SEwS8FU3R-sH~3!LxtIt%yFml{o7Uqy40<@+nfovU{#*Segw*1pjG41}+^L}2T|)%+qe)28?e}#w2z7#%r?knxj@nC5i9H^dNM|PX9Ndo8P${R{30&Y>& z2espDMGlm|!=4I+^L6yAJnO!76srBd;zHeqDnIQ4yVwv?IGy1EC7rTo%f3pL?SpK5 z$+X+vpUIAfU1PP@*S2iKi8k&8!G2%c=t})xUN(un{*N)%xdygE17!HK{8RU(j9&dup5+qsPUEE?GCi_tJ0y6{*Rbna0Ra)aNNaK`^N$2SN?rJOZvCZjA7 zm;VnQIV=MVZu|8P6GhV~XHeX!3fx@W_2e6FN%TUzSkY$J1LTF*5F4U2J|VTnmK$C zf#rr%iiAEmnff129;H#oN*=q%1T;p`1d zvULht#Px+)5Ad(fX9v3WN4*eZk?5*$5L>-U4k(UppmaozeDyTSX4f(#M6Nj4r zGev)?K3GoJZT#?y3x$A)qK^nSPQP-vPL6Yx%FACuQu`GJ?B!~x5DiGJuWC}cDhNO9 z2~gjA)c(30`Z9eZOki$SC~S(aDK=S%H+_Y(Ew?4, 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Russian (http://www.transifex.com/projects/p/django-rest-framework/language/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\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 "Недопустимый заголовок. Учетные данные некорректно закодированны в base64." + +#: 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 "Пользователь неактивен или удален." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Произошла ошибка сервера." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Искаженный запрос." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Некорректные учетные данные." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Учетные данные не были предоставлены." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "У вас нет прав для выполнения этой операции." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Не найдено." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Метод \"{method}\" не разрешен." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Невозможно удовлетворить \"Accept\" заголовок запроса." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Неподдерживаемый тип данных \"{media_type}\" в запросе." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Запрос был проигнорирован." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Это поле обязательно." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Это поле не может быть null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" не является корректным булевым значением." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Это поле не может быть пустым." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Убедитесь что в этом поле не больше {max_length} символов." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Убедитесь что в этом поле как минимум {min_length} символов." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Введите корректный адрес электронной почты." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Значение не соответствует требуемому паттерну." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Введите корректный \"slug\", состоящий из букв, цифр, знаков подчеркивания или дефисов." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Введите корректный URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Требуется целочисленное значение." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Убедитесь что значение меньше или равно {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Убедитесь что значение больше или равно {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Слишком длинное значение." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Требуется численное значение." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Убедитесь что в числе не больше {max_digits} знаков." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Убедитесь что в числе не больше {max_decimal_places} знаков в дробной части." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Убедитесь что в цисле не больше {max_whole_digits} знаков в целой части." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Неправильный формат datetime. Используйте один из этих форматов: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Ожидался datetime, но был получен date." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Неправильный формат date. Используйте один из этих форматов: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Ожидался date, но был получен datetime." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Неправильный формат времени. Используйте один из этих форматов: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" не является корректным значением." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Ожидался list со значениями, но был получен \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Не был загружен файл." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Загруженный файл не является корректным файлом. " + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Невозможно определить имя файла." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Загруженный файл пуст." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Убедитесь что имя файла меньше {max_length} символов (сейчас {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Загрузите корректное изображение. Загруженный файл не является изображением, либо является испорченным." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Недопустимая страница \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Недопустимый первичный ключ \"{pk_value}\" - объект не существует." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Некорректный тип. Ожилалось значение первичного ключа, получен {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Недопустимая ссылка - нет совпадения по URL." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Недопустимая ссылка - некорректное совпадение по URL," + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Недопустимая ссылка - объект не существует." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Некорректный тип. Ожидался URL, получен {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Объект с {slug_name}={value} не существует." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Недопустимое значение." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Недопустимые данные. Ожидался dictionary, но был получен {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Учетная запись пользователя отключена." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Невозможно войти с предоставленными учетными данными." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Должен включать \"username\" и \"password\"." diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.mo b/rest_framework/locale/sk/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..53bb95d84305891cb378d69a6f9936e7022ec7f2 GIT binary patch literal 2748 zcmb`ION+Cy>CK;A`N{2L^Ue zfo`7y-MX*9_N8!B0WR^d84MdLF`;2Zsa5@$SL<@Nn#aqdkPN zj0fB)9~VOxx5x0pWe?#hpPl%0Idl)Cl-hQIRA|lPz?&^>n@cMq zW`&XzVbTp*YN*RKC#fvuT6IM1R?n#-jmtTM^`_M$>?Y0MFSxNZ8$~>~)aESaI`B@) zNa2vk65H|+x2nTZ5z{g=A{ybi6;7RZ{AQCh^Dsw0zCViJ2shO2>h1{r9nLSoB# z$(B=2R#B=}u$Q{Rw#OAR88Elj6nU<+g(S!ljBGE*K~RIiyKVqQurVwk?0{Fqq6lX} zn4wr0cbq%d{URibtmtq;3CBF;FdTSqtAb)xI@V_RE$2ED#<*&RN{n1kP^U2Gb}Jcp zOIod>F8!0Se}QYaG^F`eCRY@lT{zdImPTX|)TDBjJN^EmSrM`Sda+{srV6RD^k&mX ziI#W9pJ$ebG&5a)#hvr325 zY^#hJnrmxetl)ISs_Z!4XgCo>Dmz*^4b25x;eL~|ETnU(>ac{S+ScY_t=8>!1M6&x z7Qg5=)KJ6DaZ}4{ap|wGadG{zzqNE|ux66lp336M#f8lX)r0BE9BvN!gWp8%LbCj# zt>r1M(qW3)OdD<+=bI<|L!0Z|@hz_X1$SR@4MRG#EbLf#NG7Q)rG;jxe*$?jA<4WP zn#TUaH2B(>l8r`v4;`av3Ta9nZOqW_-6W4R4!FrWCT9*;ybC>(^lZw=FsSJ*Gm`l3 z0>FpekGa%~cFGLm6d|hpt1(+eljF-O6|}};Ci`EY-tiNrGqrIYW9szUXn=gN zSk*}i>JSWd#Ep*Rt zg0KIJkwN<$WdNy$D+t#=x`KZz*K{YG+bT&*qdcpGvi`cWKFC22eNbb@?jx2W7_LlE z6zd88jAZ{>*>RB#c1z10R8y`xD#fDpvhVho^wwx7Az~8`t|+I%q04uA2lA z&bUIuV7qOI9Qgr8kaP#NSV0)x)B?f zaV;hsE@N1f*CJOj`d?Xa*Ctwc__rP&c=PUfL}Eg_3(Xq3w@L>4w_GGSODsH=y-`rt eGPdRn2HqRaF}_MHiG*{)g&tWRUI;08;Qa-SD|_7l literal 0 HcmV?d00001 diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.po b/rest_framework/locale/sk/LC_MESSAGES/django.po new file mode 100644 index 000000000..9dd378c05 --- /dev/null +++ b/rest_framework/locale/sk/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Stanislav Komanec , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Slovak (http://www.transifex.com/projects/p/django-rest-framework/language/sk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sk\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Nesprávna hlavička. Neboli poskytnuté prihlasovacie údaje." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Nesprávna hlavička. Prihlasovacie údaje nesmú obsahovať medzery." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Nesprávna hlavička. Prihlasovacie údaje nie sú správne zakódované pomocou metódy base64." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Nesprávne prihlasovacie údaje." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Nesprávna token hlavička. Neboli poskytnuté prihlasovacie údaje." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Nesprávna token hlavička. Token hlavička nesmie obsahovať medzery." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Nesprávny token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Daný používateľ je neaktívny, alebo zmazaný." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Vyskytla sa chyba na strane servera." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Požiadavok má nesprávny formát, alebo je poškodený." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Nesprávne prihlasovacie údaje." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Prihlasovacie údaje neboli zadané." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "K danej akcii nemáte oprávnenie." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Nebolo nájdené." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Metóda \"{method}\" nie je povolená." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Nie je možné vyhovieť požiadavku v hlavičke \"Accept\"." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Požiadavok obsahuje nepodporovaný media type: \"{media_type}\"." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Daný používateľ je zablokovaný." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "S danými prihlasovacími údajmi nebolo možné sa prihlásiť." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Musí obsahovať parametre \"používateľské meno\" a \"heslo\"." diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.mo b/rest_framework/locale/sv/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..a33b0cc58b80ad6844df00c4ef6885d4ce87a0fe GIT binary patch literal 8647 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\n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/django-rest-framework/language/sv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "Ogiltig \"basic\"-header. Inga användaruppgifter tillhandahölls." + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "Ogiltig \"basic\"-header. Strängen för användaruppgifterna ska inte innehålla mellanslag." + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "Ogiltig \"basic\"-header. Användaruppgifterna är inte korrekt base64-kodade." + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "Ogiltigt användarnamn/lösenord." + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Ogiltig \"token\"-header. Inga användaruppgifter tillhandahölls." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Ogiltig \"token\"-header. Strängen för referensen ska inte innehålla mellanslag." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Ogiltig \"token\"." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Användaren borttagen eller inaktiv." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Ett serverfel inträffade." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Ogiltig förfrågan." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Ogiltiga inloggningsuppgifter. " + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Autentiseringsuppgifter ej tillhandahållna." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Du har inte tillåtelse att utföra denna förfrågan." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Hittades inte." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "Metoden \"{method}\" tillåts inte." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "Kunde inte tillfredsställa förfrågans \"Accept\"-header." + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "Medietypen \"{media_type}\" stöds inte." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "Förfrågan stoppades eftersom du har skickat för många." + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Det här fältet är obligatoriskt." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Det här fältet får inte vara null." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" är inte ett giltigt booleskt värde." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Det här fältet får inte vara blankt." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Se till att detta fält inte har fler än {max_length} tecken." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Se till att detta fält har minst {min_length} tecken." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Ange en giltig mejladress." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Det här värdet matchar inte mallen." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Ange en giltig \"slug\" bestående av bokstäver, nummer, understreck eller bindestreck." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Ange en giltig URL." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Ett giltigt heltal krävs." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Se till att detta värde är mindre än eller lika med {max_value}." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Se till att detta värde är större än eller lika med {min_value}." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "Textvärdet är för långt." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Ett giltigt nummer krävs." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Se till att det inte finns fler än totalt {max_digits} siffror." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Se till att det inte finns fler än {max_decimal_places} decimaler." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Se till att det inte finns fler än {max_whole_digits} siffror före decimalpunkten." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datumtiden har fel format. Använd ett av dessa format istället: {format}." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Förväntade en datumtid men fick ett datum." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Datumet har fel format. Använde ett av dessa format istället: {format}." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Förväntade ett datum men fick en datumtid." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Tiden har fel format. Använd ett av dessa format istället: {format}." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" är inte ett giltigt val." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Förväntade en lista med element men fick typen \"{input_type}\"." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Ingen fil skickades." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Den skickade informationen var inte en fil. Kontrollera formulärets kodningstyp." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Inget filnamn kunde bestämmas." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Den skickade filen var tom." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Se till att det här filnamnet har högst {max_length} tecken (det har {length})." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild eller en skadad bild." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Ogiltigt sida \"{page_number}\": {message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Ogiltigt pk \"{pk_value}\" - Objektet finns inte." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Felaktig typ. Förväntade pk-värde, fick {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Ogiltig hyperlänk - Ingen URL matchade." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Ogiltig hyperlänk - Felaktig URL-matching." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Ogiltig hyperlänk - Objektet finns inte." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Felaktig typ. Förväntade URL-sträng, fick {data_type}." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "Objekt med {slug_name}={value} finns inte." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Ogiltigt värde." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Ogiltig data. Förväntade en dictionary, men fick {datatype}." + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Det här fältet måste vara unikt." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "Fälten {field_names} måste skapa ett unikt set." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Det här fältet måste vara unikt för datumet \"{date_field}\"." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Det här fältet måste vara unikt för månaden \"{date_field}\"." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Det här fältet måste vara unikt för året \"{date_field}\"." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "Ogiltig version i \"Accept\"-headern." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "Ogiltig version i URL-resursen." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Ogiltig version i värdnamnet." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Ogiltig version i förfrågningsparametern." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Användarkontot är borttaget." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Kunde inte logga in med de angivna inloggningsuppgifterna." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "Användarnamn och lösenord måste anges." diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.mo b/rest_framework/locale/tr/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..e6b848cf5b75a891dcca8c469cb348e197915c78 GIT binary patch literal 7946 zcmbuDON<;x8OPfskYtk(5+DQ;l41zNPCT==6DQ6(Hhw>3n^^X4z(}yFnXa9wOg|?5 z@OsuV2M**GE(r&OSP+j035hJAAc1eYmN+6IB|Ib}A#nhS8;Qgv;`dc|&-B>4aUiYj z{ZDn*_tjV5yZY)qH^1U>9prw1`-u;D-Z}8(&HUqf?Sr288E^$u;2*&Iz<+}>|IQD2 z-u>XCU=ut79so1&2>5I8DEJPz4?J><=RF6$2tEZ~1$Tq@-s*WLz%$?s_&reG{|P(^ z-hG?r-2;9NlyzSKKMTH8;xE9RjNbr12i`GN@Ywfnt_kP^-67Xg41b7Qd z6Z*5@G4T7KtoskJ0d8XO1o&`?=fI~KzXZyDeg}#_{H=U`KhC_D@f^4fd>It|Tmi)& z-U7ux9)!3F9tVa0BDf3u8Hg&pzk}jWZ-aM(_u(X4!AC)P{v}X>Ls0mB2gDV;Rq$c( z_uv-rEs!O>hftc(?E+=qSHP#iF31wz4?ywD--3t0|A1`C+lw=ZJkNpRZ`~5V2}*qY z7DDyYLO!U4J6hHX}DDwUa6#uyjZUS*Pw}0_v z;V0J)?wh#xa_{Gs>jT^(=N=hwNeqb}2`|wPZtnDpTaK=M<*$cIyd9KS68S`j&v28L zoWF^V&r5SbY@gf^Dc>_ zu3CtbP-iVQml_pE+zSeB42l=2vQe5D?a!!-#gj_~i_C`am(AfQ%@Hr_=}gw-r*_h+ zFfJZP>SCxb%=@NeLmkWy0^Kp`B~^}9F>ZP9nUC$-Oz0i5F_p!c4&FP1#U4hUFUUu1 z+6KMIF3ec47#r5uV#x(-0^g~f&EhF(#@=i+ADF0{^)9JSPba#QnIzqaE{d(YLLMjsVh6BRE z>9Nji?#xM%tNzG@+6GGdK9n;5!oXkxpY?qSl6Ib{u8VOQio;(w$3?;9yoosha*NP5 zGhsSnm<5iQw{WE`Cgw~qGamUMfP6$?TvVv@i{ebrRsZHmOCo=$K5 ziC>J>4Q4#seqPk&$JkwRvAKY@$IJ%0D_RzqFMQ=vgDe(eYaZdGm3Zu*Y5md(8weS{ zJLfF!n@IJ@iY93(1g)psai*>KPfnk8#D%F;+KY4Yf=G{-XPTUp4kXKsrE}EdVNUgs zpq?J+G+m4nGJ3U+3!>VPppbe1&&K{v#|*MYCG(hMJV%s0&{@qI6sd|qkJKTnuFL&} z+$2M26DAN|E$@^Lq*ma7QoxKD<&?>Kv5%ga!Ve0ltDZp+FG^85l@lj6>IAuu&8j^% zl#ZaOc1n<->jfJawMhL+^LA*-1(27IU9#&qjkYBlBMdpVn2=G>NE9uv^qNJR_0&a4 zF!MroY0t$X2VLh*XI)U0SuBfV73idkB4$bWE~}?hM#=7u59KA4y@a|?IU_Y$6hOsC z1wt^A*#(o^M$tvcu3G9~&vg2(lo+ysFB#9}3alvgMU;9iAEyqotO*C%P+pFT4L`>H zL+yE}hfevZs2%92KS5M`8RbDR`A#+lRk1IcKX9Lp@uxyg8bn z!5~hg?hr9n7Z#U5VNg~F;V}xOxj_(XDN;ppr8UAGRpE}TI*fCbyD4y#I5BuSMb-Hk z*+8aCnbc&QB*_P|aKXc)2jqE&2#Sctl>DB`MhF`B#zu4~)D@xj4Ru)FoMSmZcHDcK zcVzm|rY>)=e@5c8s2X-c9i!pXNi2!GdE9S4SEQI3b?A8=bz^nr@Yz|lAc=(3)}K0K zL^I7(Y1jJA19>-X&c-up>h!a-&4bd1z^-`+f16P|wr$_p+_t^>_%^lu$(bFGKemmF ziOndpHp2^~HA9tsG(CWnD-l>zw zoTJX>tkEGvdD6FOPi^jH*GQDN$)bUe?$12+1+BS8ND51A+&QeilRAw$@({#Z$+U*)h(#)Q@*(1$e<9V|D zf=QZ(%X~7UcC~Fbk{lk%)af8kbkID4q0$)@4csu@vtu_Mw!GQ1IZ}Jo_AR@oyz%*` zYgS*CqORIDsZKw5#oDqZX>2epg~o=XS!eSo@8ooynNHVMR2<}2mb1L&9j&v@bf{82 zYyirGMdiO|-G?eR*auxhBIU~T2f za%*(5DPIX0EoCHIvyoMO5}}UPR#;q|&GmfRJsI&RDSZO5Iwswi*|U+ak)W^II@J^m zrx;nAXkYi>L(45kJGG#f0*$^x?XRt@EhCvvGj6DLqD0AIF2~GDk`;!LN1_>SNHpbm?uK$f|2A$p*Wu z@p0;^PBj&Wv8t*kHJ%&l4z7Nj4mIZjmgi4|EcUeSDk@*fqaa^frn?ZM_~p;KLDc3e z%WPUEROc&I$;3hH5Md*BFBDPrA>~8oh^v=}tC#z@8|77*E6W_#)?ca4Yjmy}IhOG1 zuU=k#^*u5>rjrFu1-h@pt=KFhaF)6fQV5dChY9$(u*K{X386f>xsbI>*95I}n(GC# zX%1*JMe$@sTTPLWW3d65#`H09Xz-4qZ?|HUKBrpgH(W@%Ib9ktB7GF6l95ntNh>LW zjfCX^uX zBc+sFojA2aT8-rd;ptIss3=7qF$of&`x=W%*Qs4{sjsEZuD-q$h{4B;`gy6m@o5eF ziRM(9eTBTO*Op0h8M(Kq&Xw8HZ2}JrxH(Fp>mprWM{tZZE44v3J(@4J*7;K}x_UWT zy&RVDhZ*SBP`PiHq>yl&>bi1T`a?K4{BkRyVDQU%=ar?1PA(v!3aGeP4 z9}c4EW~39UF)36e-q-TVx~P`oL@LwxfxK323aaiSM+jr*pw;{vyuRj~mq|0l=~{JH zU+4R0xoK%iCYWABC7m2j@juZ(0?Qjxts_BUHyoVy5Ka55mzPXL&uSO=bVF544Y^pp zC*(tV&oFl#q&onrVJCNl( z1|_H@E^7Ybn&k-!8a2D%x(mLe7R^a&`Op`#Rp)jfMOAw3fi$?%d2<9^A~8vQkr*wN F{$KQm6!!oC literal 0 HcmV?d00001 diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.po b/rest_framework/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 000000000..5aabbebad --- /dev/null +++ b/rest_framework/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,328 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Ertaç Paprat , 2015 +# Mesut Can Gürle , 2015 +# Recep KIRMIZI , 2015 +# Ülgen Sarıkavak , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Turkish (http://www.transifex.com/projects/p/django-rest-framework/language/tr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\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 "Geçersiz kullanıcı adı/parola" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "Geçersiz token başlığı. Kimlik bilgileri eksik." + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "Geçersiz token başlığı. Token'da boşluk olmamalı." + +#: authentication.py:168 +msgid "Invalid token." +msgstr "Geçersiz token." + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "Kullanıcı aktif değil ya da silinmiş." + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "Sunucu hatası oluştu." + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "Bozuk istek." + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "Giriş bilgileri hatalı." + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "Giriş bilgileri verilmedi." + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "Bu işlemi yapmak için izniniz bulunmuyor." + +#: exceptions.py:93 +msgid "Not found." +msgstr "Bulunamadı." + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "\"{method}\" metoduna izin verilmiyor." + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "İstekte desteklenmeyen medya tipi: \"{media_type}\"." + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "Bu alan zorunlu." + +#: fields.py:154 +msgid "This field may not be null." +msgstr "Bu alan boş bırakılmamalı." + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "\"{input}\" geçerli bir boolean değil." + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "Bu alan boş bırakılmamalı." + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "Bu alanın {max_length} karakterden fazla karakter barındırmadığından emin olun." + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "Bu alanın en az {min_length} karakter barındırdığından emin olun." + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "Geçerli bir e-posta adresi girin." + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "Bu değer gereken düzenli ifade deseni ile uyuşmuyor." + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "Harf, rakam, altçizgi veya tireden oluşan geçerli bir \"slug\" giriniz." + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "Geçerli bir URL girin." + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "Geçerli bir tam sayı girin." + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "Değerin {max_value} değerinden küçük ya da eşit olduğundan emin olun." + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "Değerin {min_value} değerinden büyük ya da eşit olduğundan emin olun." + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "String değeri çok uzun." + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "Geçerli bir numara gerekiyor." + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "Toplamda {max_digits} haneden fazla hane olmadığından emin olun." + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "Ondalık basamak değerinin {max_decimal_places} haneden fazla olmadığından emin olun." + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "Ondalık ayracından önce {max_whole_digits} basamaktan fazla olmadığından emin olun." + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "Datetime alanı yanlış biçimde. {format} biçimlerinden birini kullanın." + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "Datetime değeri bekleniyor, ama date değeri geldi." + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "Tarih biçimi yanlış. {format} biçimlerinden birini kullanın." + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "Date tipi beklenmekteydi, fakat datetime tipi geldi." + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "Time biçimi yanlış. {format} biçimlerinden birini kullanın." + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "\"{input}\" geçerli bir seçim değil." + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "Elemanların listesi beklenirken \"{input_type}\" alındı." + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "Hiçbir dosya verilmedi." + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "Gönderilen veri dosya değil. Formdaki kodlama tipini kontrol edin." + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "Hiçbir dosya adı belirlenemedi." + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "Gönderilen dosya boş." + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "Bu dosya adının en fazla {max_length} karakter uzunluğunda olduğundan emin olun. (şu anda {length} karakter)." + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "Geçerli bir resim yükleyin. Yüklediğiniz dosya resim değil ya da bozuk." + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "Geçersiz sayfa \"{page_number}\":{message}." + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "Geçersiz pk \"{pk_value}\" - obje bulunamadı." + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "Hatalı tip. Pk değeri beklenirken, alınan {data_type}." + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "Geçersiz bağlantı - Hiçbir URL eşleşmedi." + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "Geçersiz bağlantı - Yanlış URL eşleşmesi." + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "Geçersiz bağlantı - Obje bulunamadı." + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "Hatalı tip. URL metni bekleniyor, {data_type} alındı." + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "{slug_name}={value} değerini taşıyan obje bulunamadı." + +#: relations.py:296 +msgid "Invalid value." +msgstr "Geçersiz değer." + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "Geçersiz veri. Sözlük bekleniyordu fakat {datatype} geldi. " + +#: validators.py:22 +msgid "This field must be unique." +msgstr "Bu alan eşsiz olmalı." + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "{field_names} hep birlikte eşsiz bir küme oluşturmalılar." + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "Bu alan \"{date_field}\" tarihine göre eşsiz olmalı." + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "Bu alan \"{date_field}\" ayına göre eşsiz olmalı." + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "Bu alan \"{date_field}\" yılına göre eşsiz olmalı." + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "\"Accept\" başlığındaki sürüm geçersiz." + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "URL dizininde geçersiz versiyon." + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "Host adında geçersiz versiyon." + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "Sorgu parametresinde geçersiz versiyon." + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "Kullanıcı hesabı devre dışı bırakılmış." + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "Verilen bilgiler ile giriş sağlanamadı." + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "\"Kullanıcı Adı\" ve \"Parola\" eklenmeli." diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.mo b/rest_framework/locale/uk/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fc3350548aab3ae1539d04b74be5daabe929dc1a GIT binary patch literal 577 zcmZut!A=`75Df(hX^%Z~7^H%TsAD^!ZR%txYDgp`BBFHR?rxlo$*#S!y$wkGMt_fQ zVKzy_r6WCg#(rFn6+0E9;igRF&2RGWMCCk)KuONn!;=le>froRR^h|?FP+pzO9N+%5K2MmYPD*Gx~-n8r&>eH&F-F-bhciZIECK3 zm1(~1NvkDoM&q0D@col`i$w!#f-*SmbBSvQ^%Q8Cqiqk;g>+tp_&ynt?mMrxpDRa( zCbctdG6CJ+I_zc_P%WcLUOAbQk#&U^V9Isr<4zAYX?$i*IE!Nrr>9V->^SBeegFfc iV|^L7|2_5Ni#lB^Sf4X^dD%#i!\n" +"Language-Team: Ukrainian (http://www.transifex.com/projects/p/django-rest-framework/language/uk/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: uk\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\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 "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..6e7073bd7acb840a4c73c55518e1fac4addf4c31 GIT binary patch literal 8383 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 literal 0 HcmV?d00001 diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po new file mode 100644 index 000000000..011288591 --- /dev/null +++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po @@ -0,0 +1,325 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Lele Long , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-30 16:27+0000\n" +"Last-Translator: Thomas Christie \n" +"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-rest-framework/language/zh_CN/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_CN\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "没有提供认证信息(基本认证HTTP头无效)。" + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "认证字符串不应该包含空格(基本认证HTTP头无效)。" + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "认证字符串base64编码错误(基本认证HTTP头无效)。" + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "用户名或者密码错误。" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "没有提供认证信息(认证令牌HTTP头无效)。" + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "认证令牌字符串不应该包含空格(无效的认证令牌HTTP头)。" + +#: authentication.py:168 +msgid "Invalid token." +msgstr "认证令牌无效。" + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "用户未激活或者已删除。" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "服务器出现了错误。" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "畸形的请求。" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "不正确的身份认证凭据。" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "身份认证凭据未提供。" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "您没有执行该操作的权限。" + +#: exceptions.py:93 +msgid "Not found." +msgstr "未找到。" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "方法 “{method}” 不被允许。" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "无法满足Accept HTTP头的请求。" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "不支持请求中的媒体类型 “{media_type}”。" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "请求被限速。" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "这个字段是必填项。" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "这个值不能为 null。" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "“{input}” 不是合法的布尔值。" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "此字段不能为空。" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "请确保这个字段不能超过 {max_length} 个字符。" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "请确保这个字段至少包含 {min_length} 个字符。" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "请输入合法的邮件地址。" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "输入值不匹配要求的模式。" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "请输入合法的“短语“,只能包含字母,数字,下划线或者中划线。" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "请输入合法的URL。" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "请填写合法的整数值。" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "请确保该值小于或者等于 {max_value}。" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "请确保该值大于或者等于 {min_value}。" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "字符值太长。" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "请填写合法的数字。" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "请确保总计不超过 {max_digits} 个数字。" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "请确保总计不超过 {max_decimal_places} 个小数位。" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "请确保小数点前不超过 {max_whole_digits} 个数字。" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "日期时间格式错误。请从这些格式中选择:{format}。" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "期望为日期时间,得到的是日期。" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "日期格式错误。请从这些格式中选择:{format}。" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "期望为日期,得到的是日期时间。" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "时间格式错误。请从这些格式中选择:{format}。" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "“{input}” 不是合法选项。" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "期望为一个包含物件的列表,得到的类型是“{input_type}”。" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "没有提交任何文件。" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "提交的数据不是一个文件。请检查表单的编码类型。" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "无法检测到文件名。" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "提交的是空文件。" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "确保该文件名最多包含 {max_length} 个字符 ( 当前长度为{length} ) 。" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "请上传有效图片。您上传的该文件不是图片或者图片已经损坏。" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "无效页面 “{page_number}”:{message}。" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "无效主键 “{pk_value}” - 对象不存在。" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "类型错误。期望为主键,得到的类型为 {data_type}。" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "无效超链接 -没有匹配的URL。" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "无效超链接 -错误的URL匹配。" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "无效超链接 -对象不存在。" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "类型错误。期望为URL字符串,得到的类型是 {data_type}。" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "属性 {slug_name} 为 {value} 的对象不存在。" + +#: relations.py:296 +msgid "Invalid value." +msgstr "无效值。" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "无效数据。期待为字典类型,得到的是 {datatype} 。" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "该字段必须唯一。" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "字段 {field_names} 必须能构成唯一集合。" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "该字段必须在日期 “{date_field}” 唯一。" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "该字段必须在月份 “{date_field}” 唯一。" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "该字段必须在年 “{date_field}” 唯一。" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "“Accept” HTTP头包含无效版本。" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "URl路径包含无效版本。" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "主机名包含无效版本。" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +msgstr "请求参数里包含无效版本。" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "用户账户已禁用。" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "无法使用提供的认证信息登录。" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "必须包含 “用户名” 和 “密码”。" diff --git a/rest_framework/views.py b/rest_framework/views.py index 12bb78bd9..995ddd0f6 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -2,12 +2,10 @@ 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 from django.utils.encoding import smart_text +from django.utils.translation import ugettext_lazy as _ from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions from rest_framework.compat import HttpResponseBase, View @@ -15,6 +13,8 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.utils import formatting +import inspect +import warnings def get_view_name(view_cls, suffix=None): @@ -74,11 +74,11 @@ def exception_handler(exc, context): return Response(data, status=exc.status_code, headers=headers) elif isinstance(exc, Http404): - data = {'detail': 'Not found'} + data = {'detail': _('Not found.')} return Response(data, status=status.HTTP_404_NOT_FOUND) elif isinstance(exc, PermissionDenied): - data = {'detail': 'Permission denied'} + data = {'detail': _('Permission denied.')} return Response(data, status=status.HTTP_403_FORBIDDEN) # Note: Unhandled exceptions will raise a 500 error. From 53b29f0902a52f7020c95ab7488a61208b8ee8a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 31 Jan 2015 08:27:17 +0000 Subject: [PATCH 136/301] _closable_objects as an empty list, not deleted --- rest_framework/response.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/response.py b/rest_framework/response.py index 7f90bae10..c21c60a2e 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -86,8 +86,9 @@ class Response(SimpleTemplateResponse): state = super(Response, self).__getstate__() for key in ( 'accepted_renderer', 'renderer_context', 'resolver_match', - 'client', 'request', 'wsgi_request', '_closable_objects' + 'client', 'request', 'wsgi_request' ): if key in state: del state[key] + state['_closable_objects'] = [] return state From 2cc4cb24652366c6622af08370a0c04b429aa4b8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 31 Jan 2015 08:53:40 +0000 Subject: [PATCH 137/301] Fix error text in test. --- docs/topics/3.1-announcement.md | 4 +++- rest_framework/views.py | 7 +++++-- tests/test_generics.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 3eb52f4cf..5f4a8d45c 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -4,7 +4,7 @@ #### Pagination controls in the browsable API. -#### New pagination schemes. +#### New schemes, including cursor pagination. #### Support for header-based pagination. @@ -12,4 +12,6 @@ ## Internationalization +## New fields + ## ModelSerializer API diff --git a/rest_framework/views.py b/rest_framework/views.py index 995ddd0f6..9445c840c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -4,6 +4,7 @@ Provides an APIView class that is the base of all views in REST framework. from __future__ import unicode_literals from django.core.exceptions import PermissionDenied from django.http import Http404 +from django.utils import six from django.utils.encoding import smart_text from django.utils.translation import ugettext_lazy as _ from django.views.decorators.csrf import csrf_exempt @@ -74,11 +75,13 @@ def exception_handler(exc, context): return Response(data, status=exc.status_code, headers=headers) elif isinstance(exc, Http404): - data = {'detail': _('Not found.')} + msg = _('Not found.') + data = {'detail': six.text_type(msg)} return Response(data, status=status.HTTP_404_NOT_FOUND) elif isinstance(exc, PermissionDenied): - data = {'detail': _('Permission denied.')} + msg = _('Permission denied.') + data = {'detail': six.text_type(msg)} return Response(data, status=status.HTTP_403_FORBIDDEN) # Note: Unhandled exceptions will raise a 500 error. diff --git a/tests/test_generics.py b/tests/test_generics.py index fba8718f5..88e792cea 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -483,7 +483,7 @@ class TestFilterBackendAppliedToViews(TestCase): request = factory.get('/1') response = instance_view(request, pk=1).render() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.data, {'detail': 'Not found'}) + self.assertEqual(response.data, {'detail': 'Not found.'}) def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): """ From e63f49bd1d55501f766ca2e3f9c0c9fa3cfa19ab Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 31 Jan 2015 19:59:52 +0000 Subject: [PATCH 138/301] Fix field mappings for 1.8 fields --- 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 a3b8196bd..a91fe23e0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1330,13 +1330,13 @@ class ModelSerializer(Serializer): if hasattr(models, 'UUIDField'): - ModelSerializer._field_mapping[models.UUIDField] = UUIDField + ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField if postgres_fields: class CharMappingField(DictField): child = CharField() - ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField class HyperlinkedModelSerializer(ModelSerializer): From 37dce89354ab2c94fefeb0a20b6265fef98caddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Sun, 1 Feb 2015 15:33:34 -0400 Subject: [PATCH 139/301] =?UTF-8?q?Add=20support=20for=20Django=201.8?= =?UTF-8?q?=E2=80=99s=20ArrayField?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a91fe23e0..520b97748 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1337,6 +1337,7 @@ if postgres_fields: child = CharField() ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField class HyperlinkedModelSerializer(ModelSerializer): From aaa1fcd5d1137a8a32d4923a331032ffd9877975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Sun, 1 Feb 2015 16:18:02 -0400 Subject: [PATCH 140/301] Fixes #2493 --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 80e869ea6..ceb23a020 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -97,7 +97,7 @@ The first thing we need to get started on our Web API is to provide a way of ser class SnippetSerializer(serializers.Serializer): pk = serializers.IntegerField(read_only=True) title = serializers.CharField(required=False, allow_blank=True, max_length=100) - code = serializers.CharField(style={'type': 'textarea'}) + code = serializers.CharField(style={'base_template': 'textarea.html'}) linenos = serializers.BooleanField(required=False) language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') From 9437d9b8eef9ef2b0f5f69257c8515416527c5a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Sun, 1 Feb 2015 16:18:40 -0400 Subject: [PATCH 141/301] Fix base_template name in example --- docs/topics/3.0-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 24e69c2de..59fe779ca 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -826,7 +826,7 @@ The `style` keyword argument can be used to pass through additional information For example, to use a `textarea` control instead of the default `input` control, you would use the following… additional_notes = serializers.CharField( - style={'base_template': 'text_area.html'} + style={'base_template': 'textarea.html'} ) Similarly, to use a radio button control instead of the default `select` control, you would use the following… From 2111a99b320c546045f4660dec05f17b931935ca Mon Sep 17 00:00:00 2001 From: Dustin Farris Date: Sun, 1 Feb 2015 16:00:24 -0500 Subject: [PATCH 142/301] Update link for ember-django-adapter --- docs/topics/third-party-resources.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 6f4df2886..e26e3a2fa 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -237,7 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. * [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok * [drf-extensions][drf-extensions] - A collection of custom extensions -* [ember-data-django-rest-adapter][ember-data-django-rest-adapter] - An ember-data adapter +* [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js ## Other Resources @@ -309,7 +309,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework [drf-extensions]: https://github.com/chibisov/drf-extensions -[ember-data-django-rest-adapter]: https://github.com/toranb/ember-data-django-rest-adapter +[ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter [beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786 [getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html [end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework From 8f1d42e7d5146e19842d2837259284f8730b451d Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 2 Feb 2015 10:50:54 +0200 Subject: [PATCH 143/301] Fixed typos in docstrings. --- 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 42d1e3700..2fd907ecd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -633,11 +633,11 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): If we don't do this explicitly they'd get a less helpful error when calling `.save()` on the serializer. - We don't *automatically* support these sorts of nested writes brecause + We don't *automatically* support these sorts of nested writes because there are too many ambiguities to define a default behavior. Eg. Suppose we have a `UserSerializer` with a nested profile. How should - we handle the case of an update, where the `profile` realtionship does + we handle the case of an update, where the `profile` relationship does not exist? Any of the following might be valid: * Raise an application error. From 4b65e9e42be068ad3e742692262451f8836f09d3 Mon Sep 17 00:00:00 2001 From: Jason Yan Date: Mon, 2 Feb 2015 16:14:34 -0800 Subject: [PATCH 144/301] Fixed missing whitespace in error string. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2fd907ecd..d76658b03 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -177,7 +177,7 @@ class BaseSerializer(Field): ) assert hasattr(self, 'initial_data'), ( - 'Cannot call `.is_valid()` as no `data=` keyword argument was' + 'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'passed when instantiating the serializer instance.' ) From 77d061d234e03004f34058028707ecddfc730fae Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Wed, 28 Jan 2015 17:08:34 -0800 Subject: [PATCH 145/301] Provide rest_framework.resolve. Fixes #2489 --- rest_framework/relations.py | 7 ++--- rest_framework/reverse.py | 15 ++++++++++- rest_framework/versioning.py | 16 ++++++++++++ tests/test_relations.py | 50 ++++++++++++++++++++++++++++++++++-- tests/urls.py | 4 +-- 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 66857a413..809d3db9e 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,7 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404 +from django.core.urlresolvers import get_script_prefix, NoReverseMatch, Resolver404 from django.db.models.query import QuerySet from django.utils import six from django.utils.encoding import smart_text @@ -9,7 +9,7 @@ from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import OrderedDict from rest_framework.fields import get_attribute, empty, Field -from rest_framework.reverse import reverse +from rest_framework.reverse import reverse, resolve from rest_framework.utils import html @@ -205,6 +205,7 @@ class HyperlinkedRelatedField(RelatedField): return self.reverse(view_name, kwargs=kwargs, request=request, format=format) def to_internal_value(self, data): + request = self.context.get('request', None) try: http_prefix = data.startswith(('http:', 'https:')) except AttributeError: @@ -218,7 +219,7 @@ class HyperlinkedRelatedField(RelatedField): data = '/' + data[len(prefix):] try: - match = self.resolve(data) + match = self.resolve(data, request=request) except Resolver404: self.fail('no_match') diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index 8fcca55ba..0d1d94a7b 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -1,12 +1,25 @@ """ -Provide reverse functions that return fully qualified URLs +Provide urlresolver functions that return fully qualified URLs or view names """ from __future__ import unicode_literals from django.core.urlresolvers import reverse as django_reverse +from django.core.urlresolvers import resolve as django_resolve from django.utils import six from django.utils.functional import lazy +def resolve(path, urlconf=None, request=None): + """ + If versioning is being used then we pass any `resolve` calls through + to the versioning scheme instance, so that the resulting view name + can be modified if needed. + """ + scheme = getattr(request, 'versioning_scheme', None) + if scheme is not None: + return scheme.resolve(path, urlconf, request) + return django_resolve(path, urlconf) + + def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): """ If versioning is being used then we pass any `reverse` calls through diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index a07b629fe..a76da17a7 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -1,6 +1,8 @@ # coding: utf-8 from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import resolve as django_resolve +from django.core.urlresolvers import ResolverMatch from rest_framework import exceptions from rest_framework.compat import unicode_http_header from rest_framework.reverse import _reverse @@ -24,6 +26,9 @@ class BaseVersioning(object): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): return _reverse(viewname, args, kwargs, request, format, **extra) + def resolve(self, path, urlconf=None): + return django_resolve(path, urlconf) + def is_allowed_version(self, version): if not self.allowed_versions: return True @@ -127,6 +132,17 @@ class NamespaceVersioning(BaseVersioning): viewname, args, kwargs, request, format, **extra ) + def resolve(self, path, urlconf=None, request=None): + match = django_resolve(path, urlconf) + if match.namespace: + _, view_name = match.view_name.split(':') + return ResolverMatch(func=match.func, + args=match.args, + kwargs=match.kwargs, + url_name=view_name, + app_name=match.app_name) + return match + class HostNameVersioning(BaseVersioning): """ diff --git a/tests/test_relations.py b/tests/test_relations.py index fbe176e24..b82a1f2a5 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,11 +1,28 @@ from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset -from django.core.exceptions import ImproperlyConfigured +from django.conf.urls import patterns, url, include +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.utils.datastructures import MultiValueDict from rest_framework import serializers from rest_framework.fields import empty -from rest_framework.test import APISimpleTestCase +from rest_framework.test import APISimpleTestCase, APIRequestFactory +from rest_framework.versioning import NamespaceVersioning import pytest +factory = APIRequestFactory() +request = factory.get('/') # Just to ensure we have a request in the serializer context + +dummy_view = lambda request, pk: None + +included_patterns = [ + url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') +] + +urlpatterns = patterns( + '', + url(r'^v1/', include(included_patterns, namespace='v1')), + url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') +) + class TestStringRelatedField(APISimpleTestCase): def setUp(self): @@ -48,6 +65,35 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk +class TestHyperlinkedRelatedField(APISimpleTestCase): + urls = 'tests.test_relations' + + def setUp(self): + class HyperlinkedMockQueryset(MockQueryset): + def get(self, **lookup): + for item in self.items: + if item.pk == int(lookup.get('pk', -1)): + return item + raise ObjectDoesNotExist() + + self.queryset = HyperlinkedMockQueryset([ + MockObject(pk=1, name='foo'), + MockObject(pk=2, name='bar'), + MockObject(pk=3, name='baz') + ]) + self.field = serializers.HyperlinkedRelatedField( + view_name='example-detail', + queryset=self.queryset + ) + request = factory.post('/') + request.versioning_scheme = NamespaceVersioning() + self.field._context = {'request': request} + + def test_bug_2489(self): + self.field.to_internal_value('/example/3/') + self.field.to_internal_value('/v1/example/3/') + + class TestHyperlinkedIdentityField(APISimpleTestCase): def setUp(self): self.instance = MockObject(pk=1, name='foo') diff --git a/tests/urls.py b/tests/urls.py index 41f527dfd..742e361d7 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,6 @@ """ Blank URLConf just to keep the test suite happy """ -from django.conf.urls import patterns +from tests import test_relations -urlpatterns = patterns('') +urlpatterns = test_relations.urlpatterns From f3067a7fabdd0edb5bc5f48cfdadd2850866c189 Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Mon, 2 Feb 2015 20:41:06 -0800 Subject: [PATCH 146/301] Remove unnecessary APIRequestFactory get from tests. --- tests/test_relations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index b82a1f2a5..ff377d383 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -9,7 +9,6 @@ from rest_framework.versioning import NamespaceVersioning import pytest factory = APIRequestFactory() -request = factory.get('/') # Just to ensure we have a request in the serializer context dummy_view = lambda request, pk: None From d015534d530dfdc2bbd7e301ec79fed533d55032 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Feb 2015 09:15:42 +0000 Subject: [PATCH 147/301] Fleshing out 3.1 announcement --- docs/topics/3.1-announcement.md | 69 ++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 5f4a8d45c..73cccb1c3 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -1,17 +1,82 @@ # Django REST framework 3.1 +The 3.1 release is an intermediate step in the Kickstarter project releases, and includes a range of new functionality. + ## Pagination +The pagination API has been improved, making it both easier to use, and more powerful. + +#### New pagination schemes. + +Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default. + +The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. Credit to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject. + #### Pagination controls in the browsable API. -#### New schemes, including cursor pagination. +Paginated results now include controls that render directly in the browsable API. If you're using the page or limit/offset style, then you'll see a page based control displayed in the browsable API. + +**IMAGE** + +The cursor based pagination renders a more simple 'Previous'/'Next' control. + +**IMAGE** #### Support for header-based pagination. +The pagination API was previously only able to alter the pagination style in the body of the response. The API now supports being able to write pagination information in response headers, making it possible to use pagination schemes that use the `Link` or `Content-Range` headers. + +**TODO**: Link to docs. + ## Versioning +We've made it easier to build versioned APIs. Built-in schemes for versioning include both URL based and Accept header based variations. + +When using a URL based scheme, hyperlinked serializers will resolve relationships to the same API version as used on the incoming request. + +**TODO**: Example. + ## Internationalization -## New fields +REST framework now includes a built-in set of translations, and supports internationalized error responses. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header. + +**TODO**: Example. + +**TODO**: Credit. + +## New field types + +Django 1.8's new `ArrayField`, `HStoreField` and `UUIDField` are now all fully supported. + +This work also means that we now have both `serializers.DictField()`, and `serializers.ListField()` types, allowing you to express and validate a wider set of representations. ## ModelSerializer API + +The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships. + +**TODO**: Link to docs. + +## Moving packages out of core + +We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths. + +We're making this change in order to distribute the maintainance workload, and keep better focus of the core essentials of the framework. + +The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) is now our recommended option for integrating OAuth support. + +**TODO** Links and package names + +* XML +* YAML +* JSONP +* OAuth + +# What's next? + +The next focus will be on HTML renderings of API output and will include: + +* HTML form rendering of serializers. +* Filtering controls built-in to the browsable API. +* An alternative admin-style interface. + +This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release. \ No newline at end of file From 030f01afdbcd4018a288250ef1f4c12de28e63bb Mon Sep 17 00:00:00 2001 From: Brandon Cazander Date: Tue, 3 Feb 2015 02:14:38 -0800 Subject: [PATCH 148/301] Reorganize tests. --- tests/test_relations.py | 49 ++-------------------------------------- tests/test_versioning.py | 41 +++++++++++++++++++++++++++++++-- tests/urls.py | 4 ++-- 3 files changed, 43 insertions(+), 51 deletions(-) diff --git a/tests/test_relations.py b/tests/test_relations.py index ff377d383..fbe176e24 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,27 +1,11 @@ from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset -from django.conf.urls import patterns, url, include -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import MultiValueDict from rest_framework import serializers from rest_framework.fields import empty -from rest_framework.test import APISimpleTestCase, APIRequestFactory -from rest_framework.versioning import NamespaceVersioning +from rest_framework.test import APISimpleTestCase import pytest -factory = APIRequestFactory() - -dummy_view = lambda request, pk: None - -included_patterns = [ - url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') -] - -urlpatterns = patterns( - '', - url(r'^v1/', include(included_patterns, namespace='v1')), - url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') -) - class TestStringRelatedField(APISimpleTestCase): def setUp(self): @@ -64,35 +48,6 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk -class TestHyperlinkedRelatedField(APISimpleTestCase): - urls = 'tests.test_relations' - - def setUp(self): - class HyperlinkedMockQueryset(MockQueryset): - def get(self, **lookup): - for item in self.items: - if item.pk == int(lookup.get('pk', -1)): - return item - raise ObjectDoesNotExist() - - self.queryset = HyperlinkedMockQueryset([ - MockObject(pk=1, name='foo'), - MockObject(pk=2, name='bar'), - MockObject(pk=3, name='baz') - ]) - self.field = serializers.HyperlinkedRelatedField( - view_name='example-detail', - queryset=self.queryset - ) - request = factory.post('/') - request.versioning_scheme = NamespaceVersioning() - self.field._context = {'request': request} - - def test_bug_2489(self): - self.field.to_internal_value('/example/3/') - self.field.to_internal_value('/v1/example/3/') - - class TestHyperlinkedIdentityField(APISimpleTestCase): def setUp(self): self.instance = MockObject(pk=1, name='foo') diff --git a/tests/test_versioning.py b/tests/test_versioning.py index c44f727d2..e7c8485ee 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -1,9 +1,13 @@ +from .utils import MockObject, MockQueryset from django.conf.urls import include, url +from django.core.exceptions import ObjectDoesNotExist +from rest_framework import serializers from rest_framework import status, 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 +from rest_framework.test import APIRequestFactory, APITestCase, APISimpleTestCase +from rest_framework.versioning import NamespaceVersioning class RequestVersionView(APIView): @@ -29,15 +33,18 @@ class RequestInvalidVersionView(APIView): factory = APIRequestFactory() mock_view = lambda request: None +dummy_view = lambda request, pk: None included_patterns = [ url(r'^namespaced/$', mock_view, name='another'), + url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') ] urlpatterns = [ url(r'^v1/', include(included_patterns, namespace='v1')), url(r'^another/$', mock_view, name='another'), - url(r'^(?P[^/]+)/another/$', mock_view, name='another') + url(r'^(?P[^/]+)/another/$', mock_view, name='another'), + url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') ] @@ -221,3 +228,33 @@ class TestInvalidVersion: request.resolver_match = FakeResolverMatch response = view(request, version='v3') assert response.status_code == status.HTTP_404_NOT_FOUND + + +class TestHyperlinkedRelatedField(APISimpleTestCase): + urls = 'tests.test_versioning' + + def setUp(self): + + class HyperlinkedMockQueryset(MockQueryset): + def get(self, **lookup): + for item in self.items: + if item.pk == int(lookup.get('pk', -1)): + return item + raise ObjectDoesNotExist() + + self.queryset = HyperlinkedMockQueryset([ + MockObject(pk=1, name='foo'), + MockObject(pk=2, name='bar'), + MockObject(pk=3, name='baz') + ]) + self.field = serializers.HyperlinkedRelatedField( + view_name='example-detail', + queryset=self.queryset + ) + request = factory.post('/', urlconf='tests.test_versioning') + request.versioning_scheme = NamespaceVersioning() + self.field._context = {'request': request} + + def test_bug_2489(self): + self.field.to_internal_value('/example/3/') + self.field.to_internal_value('/v1/example/3/') diff --git a/tests/urls.py b/tests/urls.py index 742e361d7..41f527dfd 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,6 +1,6 @@ """ Blank URLConf just to keep the test suite happy """ -from tests import test_relations +from django.conf.urls import patterns -urlpatterns = test_relations.urlpatterns +urlpatterns = patterns('') From f6765696610a0de3cf7d9986a2dfab40ca37e88b Mon Sep 17 00:00:00 2001 From: James Cooke Date: Tue, 3 Feb 2015 13:43:03 +0000 Subject: [PATCH 149/301] Small documentation fixes * Remove "you you" from viewsets API-guide * Fix link from routers API-guide to viewsets API-guide --- docs/api-guide/routers.md | 2 +- docs/api-guide/viewsets.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 592f7d66f..222b6cd25 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -304,7 +304,7 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names]. [cite]: http://guides.rubyonrails.org/routing.html -[route-decorators]: viewsets.html#marking-extra-actions-for-routing +[route-decorators]: viewsets.md#marking-extra-actions-for-routing [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db]: http://wq.io/wq.db [wq.db-router]: http://wq.io/docs/app.py diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index b09dfc9e9..bbf92c6ce 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -201,7 +201,7 @@ Note that you can use any of the standard attributes or method overrides provide def get_queryset(self): return self.request.user.accounts.all() -Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you you will have to specify the `base_name` kwarg as part of your [router registration][routers]. +Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you will have to specify the `base_name` kwarg as part of your [router registration][routers]. Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. From 76efbdddb69d0e7279c1b9de066e829f34019609 Mon Sep 17 00:00:00 2001 From: Warren Jin Date: Tue, 3 Feb 2015 17:18:54 -0500 Subject: [PATCH 150/301] docs --- docs/api-guide/fields.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b3d274ddb..f379ac720 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -567,6 +567,10 @@ The [drf-compound-fields][drf-compound-fields] package provides "compound" seria The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes. +## djangrestframework-recursive + +the [djangorestframework-recursive][djangorestframework-recursive] package provides a `RecursiveField` for serializing and deserializing recursive structures + ## django-rest-framework-gis The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer. @@ -583,6 +587,7 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: http://drf-compound-fields.readthedocs.org [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields +[djangorestframework-recursive]: https://github.com/heywbj/django-rest-framework-recursive [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-hstore]: https://github.com/djangonauts/django-hstore From 8b4ce5c636a9abb33029e48f969bbdf38f97ca1f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Feb 2015 09:07:10 +0000 Subject: [PATCH 151/301] Minor authentication message improvement. --- env/pip-selfcheck.json | 1 + rest_framework/authentication.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json new file mode 100644 index 000000000..ad56313b1 --- /dev/null +++ b/env/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2015-02-04T09:06:02Z","pypi_version":"6.0.7"} \ No newline at end of file diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 11db05855..a75cd30cd 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -86,8 +86,13 @@ class BasicAuthentication(BaseAuthentication): Authenticate the userid and password against username and password. """ user = authenticate(username=userid, password=password) - if user is None or not user.is_active: + + if user is None: raise exceptions.AuthenticationFailed(_('Invalid username/password.')) + + if not user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + return (user, None) def authenticate_header(self, request): From a0374e44985172e4b8f6dc91fbc22897d2b06767 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Feb 2015 09:08:43 +0000 Subject: [PATCH 152/301] Remove erronous checkin --- env/pip-selfcheck.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json deleted file mode 100644 index ad56313b1..000000000 --- a/env/pip-selfcheck.json +++ /dev/null @@ -1 +0,0 @@ -{"last_check":"2015-02-04T09:06:02Z","pypi_version":"6.0.7"} \ No newline at end of file From 7bb5fd270da98d8957efb4bf0e4bd4679ddbcf5f Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:03:03 +0200 Subject: [PATCH 153/301] FIX: Don't default to list in method args Fixes @list_route and @detail_route so that they don't initialize their `methods` parameter as a list. In some cases the list gets cleared, and the result is that default parameter is now empty, and may get reused unexpectedly. --- rest_framework/decorators.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 325435b3f..a68227c14 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -109,10 +109,12 @@ def permission_classes(permission_classes): return decorator -def detail_route(methods=['get'], **kwargs): +def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ + if methods is None: + methods = ['get'] def decorator(func): func.bind_to_methods = methods func.detail = True @@ -121,10 +123,12 @@ def detail_route(methods=['get'], **kwargs): return decorator -def list_route(methods=['get'], **kwargs): +def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ + if methods is None: + methods = ['get'] def decorator(func): func.bind_to_methods = methods func.detail = False From 58e7bbc8ecad8016cc18f7dbd31b235cb515b785 Mon Sep 17 00:00:00 2001 From: Ofir Ovadia Date: Wed, 4 Feb 2015 16:08:41 +0200 Subject: [PATCH 154/301] Prefetching the user object when getting the token in TokenAuthentication. Since the user object is fetched 4 lines after getting Token from the database, this removes a DB query for each token-authenticated request. --- rest_framework/authentication.py | 2 +- tests/test_authentication.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 4832ad33b..f7601fb12 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -167,7 +167,7 @@ class TokenAuthentication(BaseAuthentication): def authenticate_credentials(self, key): try: - token = self.model.objects.get(key=key) + token = self.model.objects.select_related('user').get(key=key) except self.model.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 44837c4ef..caabcc214 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -202,6 +202,12 @@ class TokenAuthTests(TestCase): response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_post_json_makes_one_db_query(self): + """Ensure that authenticating a user using a token performs only one DB query""" + auth = "Token " + self.key + func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + self.assertNumQueries(1, func_to_test) + def test_post_form_failing_token_auth(self): """Ensure POSTing form over token auth without correct credentials fails""" response = self.csrf_client.post('/token/', {'example': 'example'}) From d920683237bd2eb17d110a80fc09708a67340f01 Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:13:30 +0200 Subject: [PATCH 155/301] Use inline if --- rest_framework/decorators.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index a68227c14..7604eae13 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -18,8 +18,7 @@ def api_view(http_method_names=None): Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. """ - if http_method_names is None: - http_method_names = ['GET'] + http_method_names = ['GET'] if http_method_names is None else http_method_names def decorator(func): @@ -113,8 +112,8 @@ def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ - if methods is None: - methods = ['get'] + methods = ['get'] if methods is None else methods + def decorator(func): func.bind_to_methods = methods func.detail = True @@ -127,8 +126,8 @@ def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ - if methods is None: - methods = ['get'] + methods = ['get'] if methods is None else methods + def decorator(func): func.bind_to_methods = methods func.detail = False From e13d2af1374c8a2b2146e1126d9406bfb4bbd9ec Mon Sep 17 00:00:00 2001 From: Greg Kempe Date: Wed, 4 Feb 2015 16:26:23 +0200 Subject: [PATCH 156/301] Parens around if clause --- rest_framework/decorators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 7604eae13..21de1acf4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -18,7 +18,7 @@ def api_view(http_method_names=None): Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. """ - http_method_names = ['GET'] if http_method_names is None else http_method_names + http_method_names = ['GET'] if (http_method_names is None) else http_method_names def decorator(func): @@ -112,7 +112,7 @@ def detail_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for detail requests. """ - methods = ['get'] if methods is None else methods + methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods @@ -126,7 +126,7 @@ def list_route(methods=None, **kwargs): """ Used to mark a method on a ViewSet that should be routed for list requests. """ - methods = ['get'] if methods is None else methods + methods = ['get'] if (methods is None) else methods def decorator(func): func.bind_to_methods = methods From 41b213414df57d7e39f1bbf3aaa35a1b033e89a3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Feb 2015 23:32:25 +0000 Subject: [PATCH 157/301] Updating release notes --- docs/topics/3.1-announcement.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 73cccb1c3..d59863602 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -60,16 +60,30 @@ The serializer redesign in 3.0 did not include any public API for modifying how We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths. -We're making this change in order to distribute the maintainance workload, and keep better focus of the core essentials of the framework. +We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework. -The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) is now our recommended option for integrating OAuth support. +The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support. -**TODO** Links and package names +The following packages are now moved out of core and should be separately installed: -* XML -* YAML -* JSONP -* OAuth +* OAuth - [djangorestframework-oauth](http://jpadilla.github.io/django-rest-framework-oauth/) +* XML - [djangorestframework-xml](http://jpadilla.github.io/django-rest-framework-xml) +* YAML - [djangorestframework-yaml](http://jpadilla.github.io/django-rest-framework-yaml) +* JSONP - [djangorestframework-jsonp](http://jpadilla.github.io/django-rest-framework-jsonp) + +It's worth reiterating that this change in policy shouldn't mean any work in your codebase other than adding a new requirement and modifying some import paths. For example to install XML rendering, you would now do: + + pip install djangorestframework-xml + +And modify your settings, like so: + + REST_FRAMEWORK = { + 'DEFAULT_RENDERER_CLASSES': [ + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + 'rest_framework_xml.renderers.XMLRenderer' + ] + } # What's next? From e1c45133126e0c47b8470b4cf7a43c6a7f4fca43 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Feb 2015 00:58:09 +0000 Subject: [PATCH 158/301] Fix NamespaceVersioning with hyperlinked serializer fields --- rest_framework/relations.py | 20 ++++++++----- rest_framework/reverse.py | 13 -------- rest_framework/versioning.py | 19 ++---------- tests/test_relations_hyperlink.py | 7 ++--- tests/test_versioning.py | 50 ++++++++++++++++++------------- tests/utils.py | 24 +++++++++++++++ 6 files changed, 72 insertions(+), 61 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 809d3db9e..0b7c9d864 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -1,7 +1,7 @@ # coding: utf-8 from __future__ import unicode_literals from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -from django.core.urlresolvers import get_script_prefix, NoReverseMatch, Resolver404 +from django.core.urlresolvers import get_script_prefix, resolve, NoReverseMatch, Resolver404 from django.db.models.query import QuerySet from django.utils import six from django.utils.encoding import smart_text @@ -9,7 +9,7 @@ from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import OrderedDict from rest_framework.fields import get_attribute, empty, Field -from rest_framework.reverse import reverse, resolve +from rest_framework.reverse import reverse from rest_framework.utils import html @@ -167,11 +167,10 @@ class HyperlinkedRelatedField(RelatedField): self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field) self.format = kwargs.pop('format', None) - # We include these simply for dependency injection in tests. - # We can't add them as class attributes or they would expect an + # We include this simply for dependency injection in tests. + # We can't add it as a class attributes or it would expect an # implicit `self` argument to be passed. self.reverse = reverse - self.resolve = resolve super(HyperlinkedRelatedField, self).__init__(**kwargs) @@ -219,11 +218,18 @@ class HyperlinkedRelatedField(RelatedField): data = '/' + data[len(prefix):] try: - match = self.resolve(data, request=request) + match = resolve(data) except Resolver404: self.fail('no_match') - if match.view_name != self.view_name: + try: + expected_viewname = request.versioning_scheme.get_versioned_viewname( + self.view_name, request + ) + except AttributeError: + expected_viewname = self.view_name + + if match.view_name != expected_viewname: self.fail('incorrect_match') try: diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index 0d1d94a7b..a251d99d6 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -3,23 +3,10 @@ Provide urlresolver functions that return fully qualified URLs or view names """ from __future__ import unicode_literals from django.core.urlresolvers import reverse as django_reverse -from django.core.urlresolvers import resolve as django_resolve from django.utils import six from django.utils.functional import lazy -def resolve(path, urlconf=None, request=None): - """ - If versioning is being used then we pass any `resolve` calls through - to the versioning scheme instance, so that the resulting view name - can be modified if needed. - """ - scheme = getattr(request, 'versioning_scheme', None) - if scheme is not None: - return scheme.resolve(path, urlconf, request) - return django_resolve(path, urlconf) - - def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): """ If versioning is being used then we pass any `reverse` calls through diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index a76da17a7..51b886f38 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -1,8 +1,6 @@ # coding: utf-8 from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ -from django.core.urlresolvers import resolve as django_resolve -from django.core.urlresolvers import ResolverMatch from rest_framework import exceptions from rest_framework.compat import unicode_http_header from rest_framework.reverse import _reverse @@ -26,9 +24,6 @@ class BaseVersioning(object): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): return _reverse(viewname, args, kwargs, request, format, **extra) - def resolve(self, path, urlconf=None): - return django_resolve(path, urlconf) - def is_allowed_version(self, version): if not self.allowed_versions: return True @@ -127,21 +122,13 @@ class NamespaceVersioning(BaseVersioning): def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra): if request.version is not None: - viewname = request.version + ':' + viewname + viewname = self.get_versioned_viewname(viewname, request) return super(NamespaceVersioning, self).reverse( viewname, args, kwargs, request, format, **extra ) - def resolve(self, path, urlconf=None, request=None): - match = django_resolve(path, urlconf) - if match.namespace: - _, view_name = match.view_name.split(':') - return ResolverMatch(func=match.func, - args=match.args, - kwargs=match.kwargs, - url_name=view_name, - app_name=match.app_name) - return match + def get_versioned_viewname(self, viewname, request): + return request.version + ':' + viewname class HostNameVersioning(BaseVersioning): diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index f1b882edf..aede61d2b 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from django.conf.urls import patterns, url +from django.conf.urls import url from django.test import TestCase from rest_framework import serializers from rest_framework.test import APIRequestFactory @@ -14,8 +14,7 @@ request = factory.get('/') # Just to ensure we have a request in the serializer dummy_view = lambda request, pk: None -urlpatterns = patterns( - '', +urlpatterns = [ url(r'^dummyurl/(?P[0-9]+)/$', dummy_view, name='dummy-url'), url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'), url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), @@ -24,7 +23,7 @@ urlpatterns = patterns( url(r'^nullableforeignkeysource/(?P[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'), url(r'^onetoonetarget/(?P[0-9]+)/$', dummy_view, name='onetoonetarget-detail'), url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'), -) +] # ManyToMany diff --git a/tests/test_versioning.py b/tests/test_versioning.py index e7c8485ee..cdd100656 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -1,4 +1,4 @@ -from .utils import MockObject, MockQueryset +from .utils import MockObject, MockQueryset, UsingURLPatterns from django.conf.urls import include, url from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers @@ -6,8 +6,9 @@ from rest_framework import status, 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, APISimpleTestCase +from rest_framework.test import APIRequestFactory, APITestCase from rest_framework.versioning import NamespaceVersioning +import pytest class RequestVersionView(APIView): @@ -35,18 +36,6 @@ factory = APIRequestFactory() mock_view = lambda request: None dummy_view = lambda request, pk: None -included_patterns = [ - url(r'^namespaced/$', mock_view, name='another'), - url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') -] - -urlpatterns = [ - url(r'^v1/', include(included_patterns, namespace='v1')), - url(r'^another/$', mock_view, name='another'), - url(r'^(?P[^/]+)/another/$', mock_view, name='another'), - url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') -] - class TestRequestVersion: def test_unversioned(self): @@ -121,8 +110,17 @@ class TestRequestVersion: assert response.data == {'version': None} -class TestURLReversing(APITestCase): - urls = 'tests.test_versioning' +class TestURLReversing(UsingURLPatterns, APITestCase): + included = [ + url(r'^namespaced/$', mock_view, name='another'), + url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') + ] + + urlpatterns = [ + url(r'^v1/', include(included, namespace='v1')), + url(r'^another/$', mock_view, name='another'), + url(r'^(?P[^/]+)/another/$', mock_view, name='another'), + ] def test_reverse_unversioned(self): view = ReverseView.as_view() @@ -230,10 +228,18 @@ class TestInvalidVersion: assert response.status_code == status.HTTP_404_NOT_FOUND -class TestHyperlinkedRelatedField(APISimpleTestCase): - urls = 'tests.test_versioning' +class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase): + included = [ + url(r'^namespaced/(?P\d+)/$', mock_view, name='namespaced'), + ] + + urlpatterns = [ + url(r'^v1/', include(included, namespace='v1')), + url(r'^v2/', include(included, namespace='v2')) + ] def setUp(self): + super(TestHyperlinkedRelatedField, self).setUp() class HyperlinkedMockQueryset(MockQueryset): def get(self, **lookup): @@ -248,13 +254,15 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): MockObject(pk=3, name='baz') ]) self.field = serializers.HyperlinkedRelatedField( - view_name='example-detail', + view_name='namespaced', queryset=self.queryset ) request = factory.post('/', urlconf='tests.test_versioning') request.versioning_scheme = NamespaceVersioning() + request.version = 'v1' self.field._context = {'request': request} def test_bug_2489(self): - self.field.to_internal_value('/example/3/') - self.field.to_internal_value('/v1/example/3/') + self.field.to_internal_value('/v1/namespaced/3/') + with pytest.raises(serializers.ValidationError): + self.field.to_internal_value('/v2/namespaced/3/') diff --git a/tests/utils.py b/tests/utils.py index 5b2d75864..b90349967 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,6 +2,30 @@ from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import NoReverseMatch +class UsingURLPatterns(object): + """ + Isolates URL patterns used during testing on the test class itself. + For example: + + class MyTestCase(UsingURLPatterns, TestCase): + urlpatterns = [ + ... + ] + + def test_something(self): + ... + """ + urls = __name__ + + def setUp(self): + global urlpatterns + urlpatterns = self.urlpatterns + + def tearDown(self): + global urlpatterns + urlpatterns = [] + + class MockObject(object): def __init__(self, **kwargs): self._kwargs = kwargs From f98f842827c6e79bbaa196482e3c3c549e8999c8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Feb 2015 01:24:55 +0000 Subject: [PATCH 159/301] Minor bits of test cleanup --- tests/test_versioning.py | 37 ++++++++++++++----------------------- 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index cdd100656..553463d19 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -1,6 +1,5 @@ -from .utils import MockObject, MockQueryset, UsingURLPatterns +from .utils import UsingURLPatterns from django.conf.urls import include, url -from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers from rest_framework import status, versioning from rest_framework.decorators import APIView @@ -33,8 +32,8 @@ class RequestInvalidVersionView(APIView): factory = APIRequestFactory() -mock_view = lambda request: None -dummy_view = lambda request, pk: None +dummy_view = lambda request: None +dummy_pk_view = lambda request, pk: None class TestRequestVersion: @@ -112,14 +111,14 @@ class TestRequestVersion: class TestURLReversing(UsingURLPatterns, APITestCase): included = [ - url(r'^namespaced/$', mock_view, name='another'), - url(r'^example/(?P\d+)/$', dummy_view, name='example-detail') + url(r'^namespaced/$', dummy_view, name='another'), + url(r'^example/(?P\d+)/$', dummy_pk_view, name='example-detail') ] urlpatterns = [ url(r'^v1/', include(included, namespace='v1')), - url(r'^another/$', mock_view, name='another'), - url(r'^(?P[^/]+)/another/$', mock_view, name='another'), + url(r'^another/$', dummy_view, name='another'), + url(r'^(?P[^/]+)/another/$', dummy_view, name='another'), ] def test_reverse_unversioned(self): @@ -230,7 +229,7 @@ class TestInvalidVersion: class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase): included = [ - url(r'^namespaced/(?P\d+)/$', mock_view, name='namespaced'), + url(r'^namespaced/(?P\d+)/$', dummy_view, name='namespaced'), ] urlpatterns = [ @@ -241,28 +240,20 @@ class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase): def setUp(self): super(TestHyperlinkedRelatedField, self).setUp() - class HyperlinkedMockQueryset(MockQueryset): - def get(self, **lookup): - for item in self.items: - if item.pk == int(lookup.get('pk', -1)): - return item - raise ObjectDoesNotExist() + class MockQueryset(object): + def get(self, pk): + return 'object %s' % pk - self.queryset = HyperlinkedMockQueryset([ - MockObject(pk=1, name='foo'), - MockObject(pk=2, name='bar'), - MockObject(pk=3, name='baz') - ]) self.field = serializers.HyperlinkedRelatedField( view_name='namespaced', - queryset=self.queryset + queryset=MockQueryset() ) - request = factory.post('/', urlconf='tests.test_versioning') + request = factory.get('/') request.versioning_scheme = NamespaceVersioning() request.version = 'v1' self.field._context = {'request': request} def test_bug_2489(self): - self.field.to_internal_value('/v1/namespaced/3/') + assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3' with pytest.raises(serializers.ValidationError): self.field.to_internal_value('/v2/namespaced/3/') From 48fa77c09e2198c7877a724a46230caedcc7b529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Wed, 4 Feb 2015 23:33:59 -0400 Subject: [PATCH 160/301] Add child to ListField when using ArrayField --- rest_framework/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 520b97748..84e4961b9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -986,15 +986,25 @@ class ModelSerializer(Serializer): # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. 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. 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. field_kwargs.pop('allow_blank', None) + if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): + child_model_field = model_field.base_field.base_field + child_field_class, child_field_kwargs = self.build_standard_field( + 'child', child_model_field + ) + + field_kwargs['child'] = child_field_class(**child_field_kwargs) + return field_class, field_kwargs def build_relational_field(self, field_name, relation_info): From c696b0ba0ced9527c8f4ad1bf6f71546d8fa65c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 5 Feb 2015 10:12:14 -0400 Subject: [PATCH 161/301] Fix possible nested array fields --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 84e4961b9..188219583 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -998,7 +998,7 @@ class ModelSerializer(Serializer): field_kwargs.pop('allow_blank', None) if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): - child_model_field = model_field.base_field.base_field + child_model_field = model_field.base_field child_field_class, child_field_kwargs = self.build_standard_field( 'child', child_model_field ) From fffde8a63be7660e716672c500f0f2bd66c7d345 Mon Sep 17 00:00:00 2001 From: Kaptian Date: Thu, 5 Feb 2015 13:27:26 -0800 Subject: [PATCH 162/301] Update throttling.py Use pk pseudo attribute for identifying the user (in case the user model is not the default and has a different column name for the unique id) --- rest_framework/throttling.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 0f10136d6..261fc2463 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -191,7 +191,7 @@ class UserRateThrottle(SimpleRateThrottle): def get_cache_key(self, request, view): if request.user.is_authenticated(): - ident = request.user.id + ident = request.user.pk else: ident = self.get_ident(request) @@ -239,7 +239,7 @@ class ScopedRateThrottle(SimpleRateThrottle): with the '.throttle_scope` property of the view. """ if request.user.is_authenticated(): - ident = request.user.id + ident = request.user.pk else: ident = self.get_ident(request) From 6c63ef13cd9ff8432f15d55a7268f2d402ae38e8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 00:04:20 +0000 Subject: [PATCH 163/301] Drop 2.x announcements --- docs/index.md | 10 +- docs/topics/3.1-announcement.md | 2 +- docs/topics/release-notes.md | 583 +------------------------------- env/pip-selfcheck.json | 1 + mkdocs.yml | 5 +- 5 files changed, 7 insertions(+), 594 deletions(-) create mode 100644 env/pip-selfcheck.json diff --git a/docs/index.md b/docs/index.md index c3a9c2d4f..cd243f357 100644 --- a/docs/index.md +++ b/docs/index.md @@ -195,11 +195,8 @@ General guides to using REST framework. * [Third Party Resources][third-party-resources] * [Contributing to REST framework][contributing] * [Project management][project-management] -* [2.0 Announcement][rest-framework-2-announcement] -* [2.2 Announcement][2.2-announcement] -* [2.3 Announcement][2.3-announcement] -* [2.4 Announcement][2.4-announcement] * [3.0 Announcement][3.0-announcement] +* [3.1 Announcement][3.1-announcement] * [Kickstarter Announcement][kickstarter-announcement] * [Release Notes][release-notes] * [Credits][credits] @@ -313,11 +310,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [contributing]: topics/contributing.md [project-management]: topics/project-management.md [third-party-resources]: topics/third-party-resources.md -[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md -[2.2-announcement]: topics/2.2-announcement.md -[2.3-announcement]: topics/2.3-announcement.md -[2.4-announcement]: topics/2.4-announcement.md [3.0-announcement]: topics/3.0-announcement.md +[3.1-announcement]: topics/3.1-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md [release-notes]: topics/release-notes.md [credits]: topics/credits.md diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index d59863602..89e99f82a 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -26,7 +26,7 @@ The cursor based pagination renders a more simple 'Previous'/'Next' control. The pagination API was previously only able to alter the pagination style in the body of the response. The API now supports being able to write pagination information in response headers, making it possible to use pagination schemes that use the `Link` or `Content-Range` headers. -**TODO**: Link to docs. +For more information, see the [custom pagination styles](../api-guide/pagination/#custom-pagination-styles) documentation. ## Versioning diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e0894d2d9..88732df80 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -124,598 +124,19 @@ For full details see the [3.0 release announcement](3.0-announcement.md). --- -## 2.4.x series - -### 2.4.4 - -**Date**: [3rd November 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.4+Release%22+). - -* **Security fix**: Escape URLs when replacing `format=` query parameter, as used in dropdown on `GET` button in browsable API to allow explicit selection of JSON vs HTML output. -* Maintain ordering of URLs in API root view for `DefaultRouter`. -* Fix `follow=True` in `APIRequestFactory` -* Resolve issue with invalid `read_only=True`, `required=True` fields being automatically generated by `ModelSerializer` in some cases. -* Resolve issue with `OPTIONS` requests returning incorrect information for views using `get_serializer_class` to dynamically determine serializer based on request method. - -### 2.4.3 - -**Date**: [19th September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.3+Release%22+). - -* Support translatable view docstrings being displayed in the browsable API. -* Support [encoded `filename*`][rfc-6266] in raw file uploads with `FileUploadParser`. -* Allow routers to support viewsets that don't include any list routes or that don't include any detail routes. -* Don't render an empty login control in browsable API if `login` view is not included. -* CSRF exemption performed in `.as_view()` to prevent accidental omission if overriding `.dispatch()`. -* Login on browsable API now displays validation errors. -* Bugfix: Fix migration in `authtoken` application. -* Bugfix: Allow selection of integer keys in nested choices. -* Bugfix: Return `None` instead of `'None'` in `CharField` with `allow_none=True`. -* Bugfix: Ensure custom model fields map to equivelent serializer fields more reliably. -* Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering. - -### 2.4.2 - -**Date**: [3rd September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.2+Release%22+). - -* Bugfix: Fix broken pagination for 2.4.x series. - -### 2.4.1 - -**Date**: [1st September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.1+Release%22+). - -* Bugfix: Fix broken login template for browsable API. - -### 2.4.0 - -**Date**: [29th August 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.0+Release%22+). - -**Django version requirements**: The lowest supported version of Django is now 1.4.2. - -**South version requirements**: This note applies to any users using the optional `authtoken` application, which includes an associated database migration. You must now *either* upgrade your `south` package to version 1.0, *or* instead use the built-in migration support available with Django 1.7. - -* Added compatibility with Django 1.7's database migration support. -* New test runner, using `py.test`. -* Deprecated `.model` view attribute in favor of explicit `.queryset` and `.serializer_class` attributes. The `DEFAULT_MODEL_SERIALIZER_CLASS` setting is also deprecated. -* `@detail_route` and `@list_route` decorators replace `@action` and `@link`. -* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. -* Added `NUM_PROXIES` setting for smarter client IP identification. -* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. -* Added `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0. -* Added `cache` attribute to throttles to allow overriding of default cache. -* Added `lookup_value_regex` attribute to routers, to allow the URL argument matching to be constrainted by the user. -* Added `allow_none` option to `CharField`. -* Support Django's standard `status_code` class attribute on responses. -* More intuitive behavior on the test client, as `client.logout()` now also removes any credentials that have been set. -* Bugfix: `?page_size=0` query parameter now falls back to default page size for view, instead of always turning pagination off. -* Bugfix: Always uppercase `X-Http-Method-Override` methods. -* Bugfix: Copy `filter_backends` list before returning it, in order to prevent view code from mutating the class attribute itself. -* Bugfix: Set the `.action` attribute on viewsets when introspected by `OPTIONS` for testing permissions on the view. -* Bugfix: Ensure `ValueError` raised during deserialization results in a error list rather than a single error. This is now consistent with other validation errors. -* Bugfix: Fix `cache_format` typo on throttle classes, was `"throtte_%(scope)s_%(ident)s"`. Note that this will invalidate existing throttle caches. - ---- - -## 2.3.x series - -### 2.3.14 - -**Date**: 12th June 2014 - -* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API. -* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`. -* Fix nested serializers linked through a backward foreign key relation. -* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`. -* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode. -* Fix `parse_header` argument convertion. -* Fix mediatype detection under Python 3. -* Web browsable API now offers blank option on dropdown when the field is not required. -* `APIException` representation improved for logging purposes. -* Allow source="*" within nested serializers. -* Better support for custom oauth2 provider backends. -* Fix field validation if it's optional and has no value. -* Add `SEARCH_PARAM` and `ORDERING_PARAM`. -* Fix `APIRequestFactory` to support arguments within the url string for GET. -* Allow three transport modes for access tokens when accessing a protected resource. -* Fix `QueryDict` encoding on request objects. -* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`. -* Support `blank_display_value` on `ChoiceField`. - -### 2.3.13 - -**Date**: 6th March 2014 - -* Django 1.7 Support. -* Fix `default` argument when used with serializer relation fields. -* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'. -* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`. -* Use `urandom` for token generation. -* Only use `Vary: Accept` when more than one renderer exists. - -### 2.3.12 - -**Date**: 15th January 2014 - -* **Security fix**: `OrderingField` now only allows ordering on readable serializer fields, or on fields explicitly specified using `ordering_fields`. This prevents users being able to order by fields that are not visible in the API, and exploiting the ordering of sensitive data such as password hashes. -* Bugfix: `write_only = True` fields now display in the browsable API. - -### 2.3.11 - -**Date**: 14th January 2014 - -* Added `write_only` serializer field argument. -* Added `write_only_fields` option to `ModelSerializer` classes. -* JSON renderer now deals with objects that implement a dict-like interface. -* Fix compatiblity with newer versions of `django-oauth-plus`. -* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unnecessary queryset re-evaluations. -* Bugfix: Allow defaults on BooleanFields to be properly honored when values are not supplied. -* Bugfix: Prevent double-escaping of non-latin1 URL query params when appending `format=json` params. - -### 2.3.10 - -**Date**: 6th December 2013 - -* Add in choices information for ChoiceFields in response to `OPTIONS` requests. -* Added `pre_delete()` and `post_delete()` method hooks. -* Added status code category helper functions. -* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. -* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. -* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. - -### 2.3.9 - -**Date**: 15th November 2013 - -* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`. -* Include errors in HTML forms in browsable API. -* Added JSON renderer support for numpy scalars. -* Added `transform_` hooks on serializers for easily modifying field output. -* Added `get_context` hook in `BrowsableAPIRenderer`. -* Allow serializers to be passed `files` but no `data`. -* `HTMLFormRenderer` now renders serializers directly to HTML without needing to create an intermediate form object. -* Added `get_filter_backends` hook. -* Added queryset aggregates to allowed fields in `OrderingFilter`. -* Bugfix: Fix decimal suppoprt with `YAMLRenderer`. -* Bugfix: Fix submission of unicode in browsable API through raw data form. - -### 2.3.8 - -**Date**: 11th September 2013 - -* Added `DjangoObjectPermissions`, and `DjangoObjectPermissionsFilter`. -* Support customizable exception handling, using the `EXCEPTION_HANDLER` setting. -* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. -* Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. -* Added `cache` attribute to throttles to allow overriding of default cache. -* 'Raw data' tab in browsable API now contains pre-populated data. -* 'Raw data' and 'HTML form' tab preference in browsable API now saved between page views. -* Bugfix: `required=True` argument fixed for boolean serializer fields. -* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. -* Bugfix: Client sending empty string instead of file now clears `FileField`. -* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. -* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*] - ---- - -[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. - - class DisablePaginationMixin(object): - def get_paginate_by(self, queryset=None): - if self.request.QUERY_PARAMS[self.paginate_by_param] == '0': - return None - return super(DisablePaginationMixin, self).get_paginate_by(queryset) - ---- - -### 2.3.7 - -**Date**: 16th August 2013 - -* Added `APITestClient`, `APIRequestFactory` and `APITestCase` etc... -* Refactor `SessionAuthentication` to allow esier override for CSRF exemption. -* Remove 'Hold down "Control" message from help_text' widget messaging when not appropriate. -* Added admin configuration for auth tokens. -* Bugfix: `AnonRateThrottle` fixed to not throttle authenticated users. -* Bugfix: Don't set `X-Throttle-Wait-Seconds` when throttle does not have `wait` value. -* Bugfix: Fixed `PATCH` button title in browsable API. -* Bugfix: Fix issue with OAuth2 provider naive datetimes. - -### 2.3.6 - -**Date**: 27th June 2013 - -* Added `trailing_slash` option to routers. -* Include support for `HttpStreamingResponse`. -* Support wider range of default serializer validation when used with custom model fields. -* UTF-8 Support for browsable API descriptions. -* OAuth2 provider uses timezone aware datetimes when supported. -* Bugfix: Return error correctly when OAuth non-existent consumer occurs. -* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg. -* Bugfix: Fix `ScopedRateThrottle`. - -### 2.3.5 - -**Date**: 3rd June 2013 - -* Added `get_url` hook to `HyperlinkedIdentityField`. -* Serializer field `default` argument may be a callable. -* `@action` decorator now accepts a `methods` argument. -* Bugfix: `request.user` should be still be accessible in renderer context if authentication fails. -* Bugfix: The `lookup_field` option on `HyperlinkedIdentityField` should apply by default to the url field on the serializer. -* Bugfix: `HyperlinkedIdentityField` should continue to support `pk_url_kwarg`, `slug_url_kwarg`, `slug_field`, in a pending deprecation state. -* Bugfix: Ensure we always return 404 instead of 500 if a lookup field cannot be converted to the correct lookup type. (Eg non-numeric `AutoInteger` pk lookup) - -### 2.3.4 - -**Date**: 24th May 2013 - -* Serializer fields now support `label` and `help_text`. -* Added `UnicodeJSONRenderer`. -* `OPTIONS` requests now return metadata about fields for `POST` and `PUT` requests. -* Bugfix: `charset` now properly included in `Content-Type` of responses. -* Bugfix: Blank choice now added in browsable API on nullable relationships. -* Bugfix: Many to many relationships with `through` tables are now read-only. -* Bugfix: Serializer fields now respect model field args such as `max_length`. -* Bugfix: SlugField now performs slug validation. -* Bugfix: Lazy-translatable strings now properly serialized. -* Bugfix: Browsable API now supports bootswatch styles properly. -* Bugfix: HyperlinkedIdentityField now uses `lookup_field` kwarg. - -**Note**: Responses now correctly include an appropriate charset on the `Content-Type` header. For example: `application/json; charset=utf-8`. If you have tests that check the content type of responses, you may need to update these accordingly. - -### 2.3.3 - -**Date**: 16th May 2013 - -* Added SearchFilter -* Added OrderingFilter -* Added GenericViewSet -* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets. -* Bugfix: Fix API Root view issue with DjangoModelPermissions - -### 2.3.2 - -**Date**: 8th May 2013 - -* Bugfix: Fix `TIME_FORMAT`, `DATETIME_FORMAT` and `DATE_FORMAT` settings. -* Bugfix: Fix `DjangoFilterBackend` issue, failing when used on view with queryset attribute. - -### 2.3.1 - -**Date**: 7th May 2013 - -* Bugfix: Fix breadcrumb rendering issue. - -### 2.3.0 - -**Date**: 7th May 2013 - -* ViewSets and Routers. -* ModelSerializers support reverse relations in 'fields' option. -* HyperLinkedModelSerializers support 'id' field in 'fields' option. -* Cleaner generic views. -* Support for multiple filter classes. -* FileUploadParser support for raw file uploads. -* DecimalField support. -* Made Login template easier to restyle. -* Bugfix: Fix issue with depth>1 on ModelSerializer. - -**Note**: See the [2.3 announcement][2.3-announcement] for full details. - ---- - -## 2.2.x series - -### 2.2.7 - -**Date**: 17th April 2013 - -* Loud failure when view does not return a `Response` or `HttpResponse`. -* Bugfix: Fix for Django 1.3 compatibility. -* Bugfix: Allow overridden `get_object()` to work correctly. - -### 2.2.6 - -**Date**: 4th April 2013 - -* OAuth2 authentication no longer requires unnecessary URL parameters in addition to the token. -* URL hyperlinking in browsable API now handles more cases correctly. -* Long HTTP headers in browsable API are broken in multiple lines when possible. -* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views. -* Bugfix: OAuth should fail hard when invalid token used. -* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`. - -### 2.2.5 - -**Date**: 26th March 2013 - -* Serializer support for bulk create and bulk update operations. -* Regression fix: Date and time fields return date/time objects by default. Fixes regressions caused by 2.2.2. See [#743][743] for more details. -* Bugfix: Fix 500 error is OAuth not attempted with OAuthAuthentication class installed. -* `Serializer.save()` now supports arbitrary keyword args which are passed through to the object `.save()` method. Mixins use `force_insert` and `force_update` where appropriate, resulting in one less database query. - -### 2.2.4 - -**Date**: 13th March 2013 - -* OAuth 2 support. -* OAuth 1.0a support. -* Support X-HTTP-Method-Override header. -* Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404) -* Deal with error data nicely when deserializing lists of objects. -* Extra override hook to configure `DjangoModelPermissions` for unauthenticated users. -* Bugfix: Fix regression which caused extra database query on paginated list views. -* Bugfix: Fix pk relationship bug for some types of 1-to-1 relations. -* Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed. - -### 2.2.3 - -**Date**: 7th March 2013 - -* Bugfix: Fix None values for for `DateField`, `DateTimeField` and `TimeField`. - -### 2.2.2 - -**Date**: 6th March 2013 - -* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField`. -* Cleanup: Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view. -* Bugfix for serializer data being uncacheable with pickle protocol 0. -* Bugfixes for model field validation edge-cases. -* Bugfix for authtoken migration while using a custom user model and south. - -### 2.2.1 - -**Date**: 22nd Feb 2013 - -* Security fix: Use `defusedxml` package to address XML parsing vulnerabilities. -* Raw data tab added to browsable API. (Eg. Allow for JSON input.) -* Added TimeField. -* Serializer fields can be mapped to any method that takes no args, or only takes kwargs which have defaults. -* Unicode support for view names/descriptions in browsable API. -* Bugfix: request.DATA should return an empty `QueryDict` with no data, not `None`. -* Bugfix: Remove unneeded field validation, which caused extra queries. - -**Security note**: Following the [disclosure of security vulnerabilities][defusedxml-announce] in Python's XML parsing libraries, use of the `XMLParser` class now requires the `defusedxml` package to be installed. - -The security vulnerabilities only affect APIs which use the `XMLParser` class, by enabling it in any views, or by having it set in the `DEFAULT_PARSER_CLASSES` setting. Note that the `XMLParser` class is not enabled by default, so this change should affect a minority of users. - -### 2.2.0 - -**Date**: 13th Feb 2013 - -* Python 3 support. -* Added a `post_save()` hook to the generic views. -* Allow serializers to handle dicts as well as objects. -* Deprecate `ManyRelatedField()` syntax in favor of `RelatedField(many=True)` -* Deprecate `null=True` on relations in favor of `required=False`. -* Deprecate `blank=True` on CharFields, just use `required=False`. -* Deprecate optional `obj` argument in permissions checks in favor of `has_object_permission`. -* Deprecate implicit hyperlinked relations behavior. -* Bugfix: Fix broken DjangoModelPermissions. -* Bugfix: Allow serializer output to be cached. -* Bugfix: Fix styling on browsable API login. -* Bugfix: Fix issue with deserializing empty to-many relations. -* Bugfix: Ensure model field validation is still applied for ModelSerializer subclasses with an custom `.restore_object()` method. - -**Note**: See the [2.2 announcement][2.2-announcement] for full details. - ---- - -## 2.1.x series - -### 2.1.17 - -**Date**: 26th Jan 2013 - -* Support proper 401 Unauthorized responses where appropriate, instead of always using 403 Forbidden. -* Support json encoding of timedelta objects. -* `format_suffix_patterns()` now supports `include` style URL patterns. -* Bugfix: Fix issues with custom pagination serializers. -* Bugfix: Nested serializers now accept `source='*'` argument. -* Bugfix: Return proper validation errors when incorrect types supplied for relational fields. -* Bugfix: Support nullable FKs with `SlugRelatedField`. -* Bugfix: Don't call custom validation methods if the field has an error. - -**Note**: If the primary authentication class is `TokenAuthentication` or `BasicAuthentication`, a view will now correctly return 401 responses to unauthenticated access, with an appropriate `WWW-Authenticate` header, instead of 403 responses. - -### 2.1.16 - -**Date**: 14th Jan 2013 - -* Deprecate `django.utils.simplejson` in favor of Python 2.6's built-in json module. -* Bugfix: `auto_now`, `auto_now_add` and other `editable=False` fields now default to read-only. -* Bugfix: PK fields now only default to read-only if they are an AutoField or if `editable=False`. -* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types. -* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types. -* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one - -**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [ticket 582](ticket-582) for more details. - -### 2.1.15 - -**Date**: 3rd Jan 2013 - -* Added `PATCH` support. -* Added `RetrieveUpdateAPIView`. -* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`. -* Tweak behavior of hyperlinked fields with an explicit format suffix. -* Relation changes are now persisted in `.save()` instead of in `.restore_object()`. -* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None. -* Bugfix: Partial updates should not set default values if field is not included. - -### 2.1.14 - -**Date**: 31st Dec 2012 - -* Bugfix: ModelSerializers now include reverse FK fields on creation. -* Bugfix: Model fields with `blank=True` are now `required=False` by default. -* Bugfix: Nested serializers now support nullable relationships. - -**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to separate them from regular data type fields, such as `CharField` and `IntegerField`. - -This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and referring to fields using the style `serializers.PrimaryKeyRelatedField`. - - -### 2.1.13 - -**Date**: 28th Dec 2012 - -* Support configurable `STATICFILES_STORAGE` storage. -* Bugfix: Related fields now respect the required flag, and may be required=False. - -### 2.1.12 - -**Date**: 21st Dec 2012 - -* Bugfix: Fix bug that could occur using ChoiceField. -* Bugfix: Fix exception in browsable API on DELETE. -* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. - -### 2.1.11 - -**Date**: 17th Dec 2012 - -* Bugfix: Fix issue with M2M fields in browsable API. - -### 2.1.10 - -**Date**: 17th Dec 2012 - -* Bugfix: Ensure read-only fields don't have model validation applied. -* Bugfix: Fix hyperlinked fields in paginated results. - -### 2.1.9 - -**Date**: 11th Dec 2012 - -* Bugfix: Fix broken nested serialization. -* Bugfix: Fix `Meta.fields` only working as tuple not as list. -* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. - -### 2.1.8 - -**Date**: 8th Dec 2012 - -* Fix for creating nullable Foreign Keys with `''` as well as `None`. -* Added `null=` related field option. - -### 2.1.7 - -**Date**: 7th Dec 2012 - -* Serializers now properly support nullable Foreign Keys. -* Serializer validation now includes model field validation, such as uniqueness constraints. -* Support 'true' and 'false' string values for BooleanField. -* Added pickle support for serialized data. -* Support `source='dotted.notation'` style for nested serializers. -* Make `Request.user` settable. -* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`. - -### 2.1.6 - -**Date**: 23rd Nov 2012 - -* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.) - -### 2.1.5 - -**Date**: 23rd Nov 2012 - -* Bugfix: Fix DjangoModelPermissions. - -### 2.1.4 - -**Date**: 22nd Nov 2012 - -* Support for partial updates with serializers. -* Added `RegexField`. -* Added `SerializerMethodField`. -* Serializer performance improvements. -* Added `obtain_token_view` to get tokens when using `TokenAuthentication`. -* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`. - -### 2.1.3 - -**Date**: 16th Nov 2012 - -* Added `FileField` and `ImageField`. For use with `MultiPartParser`. -* Added `URLField` and `SlugField`. -* Support for `read_only_fields` on `ModelSerializer` classes. -* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view. -* 201 Responses now return a 'Location' header. -* Bugfix: Serializer fields now respect `max_length`. - -### 2.1.2 - -**Date**: 9th Nov 2012 - -* **Filtering support.** -* Bugfix: Support creation of objects with reverse M2M relations. - -### 2.1.1 - -**Date**: 7th Nov 2012 - -* Support use of HTML exception templates. Eg. `403.html` -* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments. -* Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs. -* Bugfix: Make textareas same width as other fields in browsable API. -* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. - -### 2.1.0 - -**Date**: 5th Nov 2012 - -* **Serializer `instance` and `data` keyword args have their position swapped.** -* `queryset` argument is now optional on writable model fields. -* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments. -* Support Django's cache framework. -* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.) -* Bugfix: Support choice field in Browsable API. -* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument. - -**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. - ---- - -## 2.0.x series - -### 2.0.2 - -**Date**: 2nd Nov 2012 - -* Fix issues with pk related fields in the browsable API. - -### 2.0.1 - -**Date**: 1st Nov 2012 - -* Add support for relational fields in the browsable API. -* Added SlugRelatedField and ManySlugRelatedField. -* If PUT creates an instance return '201 Created', instead of '200 OK'. - -### 2.0.0 - -**Date**: 30th Oct 2012 - -* **Fix all of the things.** (Well, almost.) -* For more information please see the [2.0 announcement][announcement]. - -For older release notes, [please see the GitHub repo](old-release-notes). +For older release notes, [please see the version 2.x documentation](old-release-notes). [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [deprecation-policy]: #deprecation-policy [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy [defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html -[2.2-announcement]: 2.2-announcement.md -[2.3-announcement]: 2.3-announcement.md [743]: https://github.com/tomchristie/django-rest-framework/pull/743 [staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag [staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion -[announcement]: rest-framework-2-announcement.md [ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582 [rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3 -[old-release-notes]: https://github.com/tomchristie/django-rest-framework/blob/2.4.4/docs/topics/release-notes.md#04x-series +[old-release-notes]: http://tomchristie.github.io/rest-framework-2-docs/topics/release-notes#24x-series [3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22 [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json new file mode 100644 index 000000000..77237c3c5 --- /dev/null +++ b/env/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2015-02-05T23:51:53Z","pypi_version":"6.0.8"} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 89df4cea5..57b450545 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,11 +50,8 @@ pages: - ['topics/third-party-resources.md', 'Topics', 'Third Party Resources'] - ['topics/contributing.md', 'Topics', 'Contributing to REST framework'] - ['topics/project-management.md', 'Topics', 'Project management'] - - ['topics/rest-framework-2-announcement.md', 'Topics', '2.0 Announcement'] - - ['topics/2.2-announcement.md', 'Topics', '2.2 Announcement'] - - ['topics/2.3-announcement.md', 'Topics', '2.3 Announcement'] - - ['topics/2.4-announcement.md', 'Topics', '2.4 Announcement'] - ['topics/3.0-announcement.md', 'Topics', '3.0 Announcement'] + - ['topics/3.1-announcement.md', 'Topics', '3.1 Announcement'] - ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement'] - ['topics/release-notes.md', 'Topics', 'Release Notes'] - ['topics/credits.md', 'Topics', 'Credits'] From b11a98f1e765b0b3c11cd353405bde8379057194 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 00:05:42 +0000 Subject: [PATCH 164/301] Tweak gitignore --- .gitignore | 1 + env/pip-selfcheck.json | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 env/pip-selfcheck.json diff --git a/.gitignore b/.gitignore index 2bdf8f7eb..4f2b0ddf4 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ bin/ include/ lib/ local/ +env/ !.gitignore !.travis.yml diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json deleted file mode 100644 index 77237c3c5..000000000 --- a/env/pip-selfcheck.json +++ /dev/null @@ -1 +0,0 @@ -{"last_check":"2015-02-05T23:51:53Z","pypi_version":"6.0.8"} \ No newline at end of file From 09488ad4da321f5f15d6e3df348869b8f2116b4a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 00:25:03 +0000 Subject: [PATCH 165/301] Link to ModelSerializer API --- docs/topics/3.1-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 89e99f82a..d0975f5b8 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -54,7 +54,7 @@ This work also means that we now have both `serializers.DictField()`, and `seria The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships. -**TODO**: Link to docs. +For more information, see the documentation on [customizing field mappings](../api-guide/serializers/#customizing-field-mappings) for ModelSerializer classes. ## Moving packages out of core From 5bf803b6ed260d9afde47400b7d5e8912a16ecf6 Mon Sep 17 00:00:00 2001 From: Michael Marvick Date: Thu, 5 Feb 2015 19:42:36 -0800 Subject: [PATCH 166/301] Revert some of the changes made in 1-serialization.md --- docs/tutorial/1-serialization.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 458161d07..80e869ea6 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -151,7 +151,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # ReturnDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) + # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. @@ -182,8 +182,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] - + # [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}] ## Using ModelSerializers From 7f801b9a01fa7df3b081ddec803bd0d34cc3b35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 6 Feb 2015 01:09:19 -0400 Subject: [PATCH 167/301] Add trim_whitespace to CharField #2517 If set to `True` then leading and trailing whitespace is trimmed. Defaults to `True`. --- docs/api-guide/fields.md | 5 +++-- rest_framework/fields.py | 15 +++++++++++++-- tests/test_fields.py | 8 ++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 10291c12a..9c33974f9 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -138,11 +138,12 @@ A text representation. Optionally validates the text to be shorter than `max_len Corresponds to `django.db.models.fields.CharField` or `django.db.models.fields.TextField`. -**Signature:** `CharField(max_length=None, min_length=None, allow_blank=False)` +**Signature:** `CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)` - `max_length` - Validates that the input contains no more than this number of characters. - `min_length` - Validates that the input contains no fewer than this number of characters. - `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`. +- `trim_whitespace` - If set to `True` then leading and trailing whitespace is trimmed. Defaults to `True`. The `allow_null` option is also available for string fields, although its usage is discouraged in favor of `allow_blank`. It is valid to set both `allow_blank=True` and `allow_null=True`, but doing so means that there will be two differing types of empty value permissible for string representations, which can lead to data inconsistencies and subtle application bugs. @@ -524,7 +525,7 @@ As an example, let's create a field that can be used represent the class name of # We pass the object instance onto `to_representation`, # not just the field attribute. return obj - + def to_representation(self, obj): """ Serialize the object's class name. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 02d2adeff..ecf0dc47b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -555,6 +555,7 @@ class CharField(Field): def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) + self.trim_whitespace = kwargs.pop('trim_whitespace', True) max_length = kwargs.pop('max_length', None) min_length = kwargs.pop('min_length', None) super(CharField, self).__init__(**kwargs) @@ -576,10 +577,20 @@ class CharField(Field): return super(CharField, self).run_validation(data) def to_internal_value(self, data): - return six.text_type(data) + value = six.text_type(data) + + if self.trim_whitespace: + return value.strip() + + return value def to_representation(self, value): - return six.text_type(value) + representation = six.text_type(value) + + if self.trim_whitespace: + return representation.strip() + + return representation class EmailField(CharField): diff --git a/tests/test_fields.py b/tests/test_fields.py index 48ada7804..5a5418e65 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -410,6 +410,14 @@ class TestCharField(FieldValues): } field = serializers.CharField() + def test_trim_whitespace_default(self): + field = serializers.CharField() + assert field.to_representation(' abc ') == 'abc' + + def test_trim_whitespace_disabled(self): + field = serializers.CharField(trim_whitespace=False) + assert field.to_representation(' abc ') == ' abc ' + class TestEmailField(FieldValues): """ From 75ff754517c30df043de906b0a6fb0e1777570b7 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 10:12:57 +0100 Subject: [PATCH 168/301] Use twine to upload to pypi. --- setup.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index efe39d8d4..391987bc9 100755 --- a/setup.py +++ b/setup.py @@ -48,8 +48,11 @@ if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") sys.exit() - os.system("python setup.py sdist upload") - os.system("python setup.py bdist_wheel upload") + if os.system("pip freeze | grep twine") + print("twine not installed.\nUse `pip install twine`.\nExiting.") + sys.exit() + os.system("python setup.py sdist bdist_wheel") + os.system("twine upload dist/*") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") From 9dd97a0ee515089a1f818007f23460cf83159e71 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 10:23:58 +0100 Subject: [PATCH 169/301] Fixed a typo. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 391987bc9..4cdcfa86e 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ if sys.argv[-1] == 'publish': if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") sys.exit() - if os.system("pip freeze | grep twine") + if os.system("pip freeze | grep twine"): print("twine not installed.\nUse `pip install twine`.\nExiting.") sys.exit() os.system("python setup.py sdist bdist_wheel") From 53716f61527266159c285b903da98ec432e52564 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 11:37:29 +0000 Subject: [PATCH 170/301] Internationalization docs --- docs/index.md | 2 - docs/topics/3.1-announcement.md | 77 +++++- docs/topics/credits.md | 404 ---------------------------- docs/topics/internationalization.md | 41 +++ mkdocs.yml | 1 - 5 files changed, 113 insertions(+), 412 deletions(-) delete mode 100644 docs/topics/credits.md diff --git a/docs/index.md b/docs/index.md index cd243f357..23781419f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -199,7 +199,6 @@ General guides to using REST framework. * [3.1 Announcement][3.1-announcement] * [Kickstarter Announcement][kickstarter-announcement] * [Release Notes][release-notes] -* [Credits][credits] ## Development @@ -314,7 +313,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [3.1-announcement]: topics/3.1-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md [release-notes]: topics/release-notes.md -[credits]: topics/credits.md [tox]: http://testrun.org/tox/latest/ diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index d0975f5b8..080ef1d81 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -10,7 +10,7 @@ The pagination API has been improved, making it both easier to use, and more pow Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default. -The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. Credit to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject. +The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject. #### Pagination controls in the browsable API. @@ -34,15 +34,74 @@ We've made it easier to build versioned APIs. Built-in schemes for versioning in When using a URL based scheme, hyperlinked serializers will resolve relationships to the same API version as used on the incoming request. -**TODO**: Example. +For example, when using `NamespaceVersioning`, and the following hyperlinked serializer: + + class AccountsSerializer(serializer.HyperlinkedModelSerializer): + class Meta: + model = Accounts + fields = ('account_name', 'users') + +The output representation would match the version used on the incoming request. Like so: + + GET http://example.org/v2/accounts/10 # Version 'v2' + + { + "account_name": "europa", + "users": [ + "http://example.org/v2/users/12", # Version 'v2' + "http://example.org/v2/users/54", + "http://example.org/v2/users/87" + ] + } ## Internationalization REST framework now includes a built-in set of translations, and supports internationalized error responses. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header. -**TODO**: Example. +You can change the default language by using the standard Django `LANGUAGE_CODE` setting: -**TODO**: Credit. + LANGUAGE_CODE = "es-es" + +You can turn on per-request language requests by adding `LocalMiddleware` to your `MIDDLEWARE_CLASSES` setting: + + MIDDLEWARE_CLASSES = [ + ... + 'django.middleware.locale.LocaleMiddleware' + ] + +When per-request internationalization is enabled, client requests will respect the `Accept-Language` header where possible. For example, let's make a request for an unsupported media type: + +**Request** + + GET /api/users HTTP/1.1 + Accept: application/xml + Accept-Language: es-es + Host: example.org + +**Response** + + HTTP/1.0 406 NOT ACCEPTABLE + + { + "detail": "No se ha podido satisfacer la solicitud de cabecera de Accept." + } + +Note that the structure of the error responses is still the same. We still have a `details` key in the response. If needed you can modify this behavior too, by using a [custom exception handler][custom-exception-handler]. + +We include built-in translations both for standard exception cases, and for serializer validation errors. + +The full list of supported languages can be found on our [Transifex project page](https://www.transifex.com/projects/p/django-rest-framework/). + +If you only wish to support a subset of the supported languages, use Django's standard `LANGUAGES` setting: + + LANGUAGES = [ + ('de', _('German')), + ('en', _('English')), + ] + +For more details, see the [internationalization documentation](internationalization.md). + +Many thanks to [Craig Blaszczyk](https://github.com/jakul) for helping push this through. ## New field types @@ -50,6 +109,10 @@ Django 1.8's new `ArrayField`, `HStoreField` and `UUIDField` are now all fully s This work also means that we now have both `serializers.DictField()`, and `serializers.ListField()` types, allowing you to express and validate a wider set of representations. +If you're building a new 1.8 project, then you should probably consider using `UUIDField` as the primary keys for all your models. This style will work automatically with hyperlinked serializers, returning URLs in the following style: + + http://example.org/api/purchases/9b1a433f-e90d-4948-848b-300fdc26365d + ## ModelSerializer API The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships. @@ -85,6 +148,8 @@ And modify your settings, like so: ] } +Thanks go to the latest member of our maintenance team, [José Padilla](https://github.com/jpadilla/), for handling this work and taking on ownership of these packages. + # What's next? The next focus will be on HTML renderings of API output and will include: @@ -93,4 +158,6 @@ The next focus will be on HTML renderings of API output and will include: * Filtering controls built-in to the browsable API. * An alternative admin-style interface. -This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release. \ No newline at end of file +This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release. + +[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling diff --git a/docs/topics/credits.md b/docs/topics/credits.md deleted file mode 100644 index 5f0dc7522..000000000 --- a/docs/topics/credits.md +++ /dev/null @@ -1,404 +0,0 @@ -# Credits - -The following people have helped make REST framework great. - -* Tom Christie - [tomchristie] -* Marko Tibold - [markotibold] -* Paul Miller - [paulmillr] -* Sébastien Piquemal - [sebpiq] -* Carmen Wick - [cwick] -* Alex Ehlke - [aehlke] -* Alen Mujezinovic - [flashingpumpkin] -* Carles Barrobés - [txels] -* Michael Fötsch - [mfoetsch] -* David Larlet - [david] -* Andrew Straw - [astraw] -* Zeth - [zeth] -* Fernando Zunino - [fzunino] -* Jens Alm - [ulmus] -* Craig Blaszczyk - [jakul] -* Garcia Solero - [garciasolero] -* Tom Drummond - [devioustree] -* Danilo Bargen - [dbrgn] -* Andrew McCloud - [amccloud] -* Thomas Steinacher - [thomasst] -* Meurig Freeman - [meurig] -* Anthony Nemitz - [anemitz] -* Ewoud Kohl van Wijngaarden - [ekohl] -* Michael Ding - [yandy] -* Mjumbe Poe - [mjumbewu] -* Natim - [natim] -* Sebastian Żurek - [sebzur] -* Benoit C - [dzen] -* Chris Pickett - [bunchesofdonald] -* Ben Timby - [btimby] -* Michele Lazzeri - [michelelazzeri-nextage] -* Camille Harang - [mammique] -* Paul Oswald - [poswald] -* Sean C. Farley - [scfarley] -* Daniel Izquierdo - [izquierdo] -* Can Yavuz - [tschan] -* Shawn Lewis - [shawnlewis] -* Alec Perkins - [alecperkins] -* Michael Barrett - [phobologic] -* Mathieu Dhondt - [laundromat] -* Johan Charpentier - [cyberj] -* Jamie Matthews - [j4mie] -* Mattbo - [mattbo] -* Max Hurl - [maximilianhurl] -* Tomi Pajunen - [eofs] -* Rob Dobson - [rdobson] -* Daniel Vaca Araujo - [diviei] -* Madis Väin - [madisvain] -* Stephan Groß - [minddust] -* Pavel Savchenko - [asfaltboy] -* Otto Yiu - [ottoyiu] -* Jacob Magnusson - [jmagnusson] -* Osiloke Harold Emoekpere - [osiloke] -* Michael Shepanski - [mjs7231] -* Toni Michel - [tonimichel] -* Ben Konrath - [benkonrath] -* Marc Aymerich - [glic3rinu] -* Ludwig Kraatz - [ludwigkraatz] -* Rob Romano - [robromano] -* Eugene Mechanism - [mechanism] -* Jonas Liljestrand - [jonlil] -* Justin Davis - [irrelative] -* Dustin Bachrach - [dbachrach] -* Mark Shirley - [maspwr] -* Olivier Aubert - [oaubert] -* Yuri Prezument - [yprez] -* Fabian Buechler - [fabianbuechler] -* Mark Hughes - [mhsparks] -* Michael van de Waeter - [mvdwaeter] -* Reinout van Rees - [reinout] -* Michael Richards - [justanotherbody] -* Ben Roberts - [roberts81] -* Venkata Subramanian Mahalingam - [annacoder] -* George Kappel - [gkappel] -* Colin Murtaugh - [cmurtaugh] -* Simon Pantzare - [pilt] -* Szymon Teżewski - [sunscrapers] -* Joel Marcotte - [joual] -* Trey Hunner - [treyhunner] -* Roman Akinfold - [akinfold] -* Toran Billups - [toranb] -* Sébastien Béal - [sebastibe] -* Andrew Hankinson - [ahankinson] -* Juan Riaza - [juanriaza] -* Michael Mior - [michaelmior] -* Marc Tamlyn - [mjtamlyn] -* Richard Wackerbarth - [wackerbarth] -* Johannes Spielmann - [shezi] -* James Cleveland - [radiosilence] -* Steve Gregory - [steve-gregory] -* Federico Capoano - [nemesisdesign] -* Bruno Renié - [brutasse] -* Kevin Stone - [kevinastone] -* Guglielmo Celata - [guglielmo] -* Mike Tums - [mktums] -* Michael Elovskikh - [wronglink] -* Michał Jaworski - [swistakm] -* Andrea de Marco - [z4r] -* Fernando Rocha - [fernandogrd] -* Xavier Ordoquy - [xordoquy] -* Adam Wentz - [floppya] -* Andreas Pelme - [pelme] -* Ryan Detzel - [ryanrdetzel] -* Omer Katz - [thedrow] -* Wiliam Souza - [waa] -* Jonas Braun - [iekadou] -* Ian Dash - [bitmonkey] -* Bouke Haarsma - [bouke] -* Pierre Dulac - [dulaccc] -* Dave Kuhn - [kuhnza] -* Sitong Peng - [stoneg] -* Victor Shih - [vshih] -* Atle Frenvik Sveen - [atlefren] -* J Paul Reed - [preed] -* Matt Majewski - [forgingdestiny] -* Jerome Chen - [chenjyw] -* Andrew Hughes - [eyepulp] -* Daniel Hepper - [dhepper] -* Hamish Campbell - [hamishcampbell] -* Marlon Bailey - [avinash240] -* James Summerfield - [jsummerfield] -* Andy Freeland - [rouge8] -* Craig de Stigter - [craigds] -* Pablo Recio - [pyriku] -* Brian Zambrano - [brianz] -* Òscar Vilaplana - [grimborg] -* Ryan Kaskel - [ryankask] -* Andy McKay - [andymckay] -* Matteo Suppo - [matteosuppo] -* Karol Majta - [lolek09] -* David Jones - [commonorgarden] -* Andrew Tarzwell - [atarzwell] -* Michal Dvořák - [mikee2185] -* Markus Törnqvist - [mjtorn] -* Pascal Borreli - [pborreli] -* Alex Burgel - [aburgel] -* David Medina - [copitux] -* Areski Belaid - [areski] -* Ethan Freman - [mindlace] -* David Sanders - [davesque] -* Philip Douglas - [freakydug] -* Igor Kalat - [trwired] -* Rudolf Olah - [omouse] -* Gertjan Oude Lohuis - [gertjanol] -* Matthias Jacob - [cyroxx] -* Pavel Zinovkin - [pzinovkin] -* Will Kahn-Greene - [willkg] -* Kevin Brown - [kevin-brown] -* Rodrigo Martell - [coderigo] -* James Rutherford - [jimr] -* Ricky Rosario - [rlr] -* Veronica Lynn - [kolvia] -* Dan Stephenson - [etos] -* Martin Clement - [martync] -* Jeremy Satterfield - [jsatt] -* Christopher Paolini - [chrispaolini] -* Filipe A Ximenes - [filipeximenes] -* Ramiro Morales - [ramiro] -* Krzysztof Jurewicz - [krzysiekj] -* Eric Buehl - [ericbuehl] -* Kristian Øllegaard - [kristianoellegaard] -* Alexander Akhmetov - [alexander-akhmetov] -* Andrey Antukh - [niwibe] -* Mathieu Pillard - [diox] -* Edmond Wong - [edmondwong] -* Ben Reilly - [bwreilly] -* Tai Lee - [mrmachine] -* Markus Kaiserswerth - [mkai] -* Henry Clifford - [hcliff] -* Thomas Badaud - [badale] -* Colin Huang - [tamakisquare] -* Ross McFarland - [ross] -* Jacek Bzdak - [jbzdak] -* Alexander Lukanin - [alexanderlukanin13] -* Yamila Moreno - [yamila-moreno] -* Rob Hudson - [robhudson] -* Alex Good - [alexjg] -* Ian Foote - [ian-foote] -* Chuck Harmston - [chuckharmston] -* Philip Forget - [philipforget] -* Artem Mezhenin - [amezhenin] - -Many thanks to everyone who's contributed to the project. - -## Additional thanks - -The documentation is built with [Bootstrap] and [Markdown]. - -Project hosting is with [GitHub]. - -Continuous integration testing is managed with [Travis CI][travis-ci]. - -The [live sandbox][sandbox] is hosted on [Heroku]. - -Various inspiration taken from the [Rails], [Piston], [Tastypie], [Dagny] and [django-viewsets] projects. - -Development of REST framework 2.0 was sponsored by [DabApps]. - -## Contact - -For usage questions please see the [REST framework discussion group][group]. - -You can also contact [@_tomchristie][twitter] directly on twitter. - -[twitter]: http://twitter.com/_tomchristie -[bootstrap]: http://twitter.github.com/bootstrap/ -[markdown]: http://daringfireball.net/projects/markdown/ -[github]: https://github.com/tomchristie/django-rest-framework -[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework -[rails]: http://rubyonrails.org/ -[piston]: https://bitbucket.org/jespern/django-piston -[tastypie]: https://github.com/toastdriven/django-tastypie -[dagny]: https://github.com/zacharyvoase/dagny -[django-viewsets]: https://github.com/BertrandBordage/django-viewsets -[dabapps]: http://lab.dabapps.com -[sandbox]: http://restframework.herokuapp.com/ -[heroku]: http://www.heroku.com/ -[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework - -[tomchristie]: https://github.com/tomchristie -[markotibold]: https://github.com/markotibold -[paulmillr]: https://github.com/paulmillr -[sebpiq]: https://github.com/sebpiq -[cwick]: https://github.com/cwick -[aehlke]: https://github.com/aehlke -[flashingpumpkin]: https://github.com/flashingpumpkin -[txels]: https://github.com/txels -[mfoetsch]: https://github.com/mfoetsch -[david]: https://github.com/david -[astraw]: https://github.com/astraw -[zeth]: https://github.com/zeth -[fzunino]: https://github.com/fzunino -[ulmus]: https://github.com/ulmus -[jakul]: https://github.com/jakul -[garciasolero]: https://github.com/garciasolero -[devioustree]: https://github.com/devioustree -[dbrgn]: https://github.com/dbrgn -[amccloud]: https://github.com/amccloud -[thomasst]: https://github.com/thomasst -[meurig]: https://github.com/meurig -[anemitz]: https://github.com/anemitz -[ekohl]: https://github.com/ekohl -[yandy]: https://github.com/yandy -[mjumbewu]: https://github.com/mjumbewu -[natim]: https://github.com/natim -[sebzur]: https://github.com/sebzur -[dzen]: https://github.com/dzen -[bunchesofdonald]: https://github.com/bunchesofdonald -[btimby]: https://github.com/btimby -[michelelazzeri-nextage]: https://github.com/michelelazzeri-nextage -[mammique]: https://github.com/mammique -[poswald]: https://github.com/poswald -[scfarley]: https://github.com/scfarley -[izquierdo]: https://github.com/izquierdo -[tschan]: https://github.com/tschan -[shawnlewis]: https://github.com/shawnlewis -[alecperkins]: https://github.com/alecperkins -[phobologic]: https://github.com/phobologic -[laundromat]: https://github.com/laundromat -[cyberj]: https://github.com/cyberj -[j4mie]: https://github.com/j4mie -[mattbo]: https://github.com/mattbo -[maximilianhurl]: https://github.com/maximilianhurl -[eofs]: https://github.com/eofs -[rdobson]: https://github.com/rdobson -[diviei]: https://github.com/diviei -[madisvain]: https://github.com/madisvain -[minddust]: https://github.com/minddust -[asfaltboy]: https://github.com/asfaltboy -[ottoyiu]: https://github.com/OttoYiu -[jmagnusson]: https://github.com/jmagnusson -[osiloke]: https://github.com/osiloke -[mjs7231]: https://github.com/mjs7231 -[tonimichel]: https://github.com/tonimichel -[benkonrath]: https://github.com/benkonrath -[glic3rinu]: https://github.com/glic3rinu -[ludwigkraatz]: https://github.com/ludwigkraatz -[robromano]: https://github.com/robromano -[mechanism]: https://github.com/mechanism -[jonlil]: https://github.com/jonlil -[irrelative]: https://github.com/irrelative -[dbachrach]: https://github.com/dbachrach -[maspwr]: https://github.com/maspwr -[oaubert]: https://github.com/oaubert -[yprez]: https://github.com/yprez -[fabianbuechler]: https://github.com/fabianbuechler -[mhsparks]: https://github.com/mhsparks -[mvdwaeter]: https://github.com/mvdwaeter -[reinout]: https://github.com/reinout -[justanotherbody]: https://github.com/justanotherbody -[roberts81]: https://github.com/roberts81 -[annacoder]: https://github.com/annacoder -[gkappel]: https://github.com/gkappel -[cmurtaugh]: https://github.com/cmurtaugh -[pilt]: https://github.com/pilt -[sunscrapers]: https://github.com/sunscrapers -[joual]: https://github.com/joual -[treyhunner]: https://github.com/treyhunner -[akinfold]: https://github.com/akinfold -[toranb]: https://github.com/toranb -[sebastibe]: https://github.com/sebastibe -[ahankinson]: https://github.com/ahankinson -[juanriaza]: https://github.com/juanriaza -[michaelmior]: https://github.com/michaelmior -[mjtamlyn]: https://github.com/mjtamlyn -[wackerbarth]: https://github.com/wackerbarth -[shezi]: https://github.com/shezi -[radiosilence]: https://github.com/radiosilence -[steve-gregory]: https://github.com/steve-gregory -[nemesisdesign]: https://github.com/nemesisdesign -[brutasse]: https://github.com/brutasse -[kevinastone]: https://github.com/kevinastone -[guglielmo]: https://github.com/guglielmo -[mktums]: https://github.com/mktums -[wronglink]: https://github.com/wronglink -[swistakm]: https://github.com/swistakm -[z4r]: https://github.com/z4r -[fernandogrd]: https://github.com/fernandogrd -[xordoquy]: https://github.com/xordoquy -[floppya]: https://github.com/floppya -[pelme]: https://github.com/pelme -[ryanrdetzel]: https://github.com/ryanrdetzel -[thedrow]: https://github.com/thedrow -[waa]: https://github.com/wiliamsouza -[iekadou]: https://github.com/iekadou -[bitmonkey]: https://github.com/bitmonkey -[bouke]: https://github.com/bouke -[dulaccc]: https://github.com/dulaccc -[kuhnza]: https://github.com/kuhnza -[stoneg]: https://github.com/stoneg -[vshih]: https://github.com/vshih -[atlefren]: https://github.com/atlefren -[preed]: https://github.com/preed -[forgingdestiny]: https://github.com/forgingdestiny -[chenjyw]: https://github.com/chenjyw -[eyepulp]: https://github.com/eyepulp -[dhepper]: https://github.com/dhepper -[hamishcampbell]: https://github.com/hamishcampbell -[avinash240]: https://github.com/avinash240 -[jsummerfield]: https://github.com/jsummerfield -[rouge8]: https://github.com/rouge8 -[craigds]: https://github.com/craigds -[pyriku]: https://github.com/pyriku -[brianz]: https://github.com/brianz -[grimborg]: https://github.com/grimborg -[ryankask]: https://github.com/ryankask -[andymckay]: https://github.com/andymckay -[matteosuppo]: https://github.com/matteosuppo -[lolek09]: https://github.com/lolek09 -[commonorgarden]: https://github.com/commonorgarden -[atarzwell]: https://github.com/atarzwell -[mikee2185]: https://github.com/mikee2185 -[mjtorn]: https://github.com/mjtorn -[pborreli]: https://github.com/pborreli -[aburgel]: https://github.com/aburgel -[copitux]: https://github.com/copitux -[areski]: https://github.com/areski -[mindlace]: https://github.com/mindlace -[davesque]: https://github.com/davesque -[freakydug]: https://github.com/freakydug -[trwired]: https://github.com/trwired -[omouse]: https://github.com/omouse -[gertjanol]: https://github.com/gertjanol -[cyroxx]: https://github.com/cyroxx -[pzinovkin]: https://github.com/pzinovkin -[coderigo]: https://github.com/coderigo -[willkg]: https://github.com/willkg -[kevin-brown]: https://github.com/kevin-brown -[jimr]: https://github.com/jimr -[rlr]: https://github.com/rlr -[kolvia]: https://github.com/kolvia -[etos]: https://github.com/etos -[martync]: https://github.com/martync -[jsatt]: https://github.com/jsatt -[chrispaolini]: https://github.com/chrispaolini -[filipeximenes]: https://github.com/filipeximenes -[ramiro]: https://github.com/ramiro -[krzysiekj]: https://github.com/krzysiekj -[ericbuehl]: https://github.com/ericbuehl -[kristianoellegaard]: https://github.com/kristianoellegaard -[alexander-akhmetov]: https://github.com/alexander-akhmetov -[niwibe]: https://github.com/niwibe -[diox]: https://github.com/diox -[edmondwong]: https://github.com/edmondwong -[bwreilly]: https://github.com/bwreilly -[mrmachine]: https://github.com/mrmachine -[mkai]: https://github.com/mkai -[hcliff]: https://github.com/hcliff -[badale]: https://github.com/badale -[tamakisquare]: https://github.com/tamakisquare -[ross]: https://github.com/ross -[jbzdak]: https://github.com/jbzdak -[alexanderlukanin13]: https://github.com/alexanderlukanin13 -[yamila-moreno]: https://github.com/yamila-moreno -[robhudson]: https://github.com/robhudson -[alexjg]: https://github.com/alexjg -[ian-foote]: https://github.com/ian-foote -[chuckharmston]: https://github.com/chuckharmston -[philipforget]: https://github.com/philipforget -[amezhenin]: https://github.com/amezhenin diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md index fdde6c43a..3968e23d1 100644 --- a/docs/topics/internationalization.md +++ b/docs/topics/internationalization.md @@ -11,12 +11,53 @@ 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. +## Enabling internationalized APIs + +You can change the default language by using the standard Django `LANGUAGE_CODE` setting: + + LANGUAGE_CODE = "es-es" + +You can turn on per-request language requests by adding `LocalMiddleware` to your `MIDDLEWARE_CLASSES` setting: + + MIDDLEWARE_CLASSES = [ + ... + 'django.middleware.locale.LocaleMiddleware' + ] + +When per-request internationalization is enabled, client requests will respect the `Accept-Language` header where possible. For example, let's make a request for an unsupported media type: + +**Request** + + GET /api/users HTTP/1.1 + Accept: application/xml + Accept-Language: es-es + Host: example.org + +**Response** + + HTTP/1.0 406 NOT ACCEPTABLE + + {"detail": "No se ha podido satisfacer la solicitud de cabecera de Accept."} + +REST framework includes these built-in translations both for standard exception cases, and for serializer validation errors. + 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]. +#### Specifying the set of supported languages. + +By default all available languages will be supported. + +If you only wish to support a subset of the available languages, use Django's standard `LANGUAGES` setting: + + LANGUAGES = [ + ('de', _('German')), + ('en', _('English')), + ] + ## 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. diff --git a/mkdocs.yml b/mkdocs.yml index 57b450545..8aacc2dfc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -54,4 +54,3 @@ pages: - ['topics/3.1-announcement.md', 'Topics', '3.1 Announcement'] - ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement'] - ['topics/release-notes.md', 'Topics', 'Release Notes'] - - ['topics/credits.md', 'Topics', 'Credits'] From 238a3b507baa4543e3bae82a6fc9d88a0aadc5ea Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 13:50:40 +0100 Subject: [PATCH 171/301] Add Twine to the requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 00d973cdf..e5f555f5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ django-oauth2-provider>=0.2.4 # wheel for PyPI installs wheel==0.24.0 +twine==1.4.0 # MkDocs for documentation previews/deploys mkdocs==0.11.1 From 750d0c9f2b994af2ba92d2d470bbe079f9d9847c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 6 Feb 2015 13:57:08 +0100 Subject: [PATCH 172/301] Add Twine to the requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e5f555f5e..32938ab23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ django-oauth2-provider>=0.2.4 # wheel for PyPI installs wheel==0.24.0 +# twine for secured PyPI uploads twine==1.4.0 # MkDocs for documentation previews/deploys From 1d4956f61615ed32f52b2f92bb8ae31b09279a34 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 13:08:04 +0000 Subject: [PATCH 173/301] Pagination images --- docs/img/cursor-pagination.png | Bin 0 -> 12221 bytes docs/img/pages-pagination.png | Bin 0 -> 10229 bytes docs/topics/3.1-announcement.md | 33 +++++++++++++++++++++++++++----- docs_theme/css/default.css | 4 ++++ 4 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 docs/img/cursor-pagination.png create mode 100644 docs/img/pages-pagination.png diff --git a/docs/img/cursor-pagination.png b/docs/img/cursor-pagination.png new file mode 100644 index 0000000000000000000000000000000000000000..1c9c99b684564b1c4af278f96f0e016de8a28ca9 GIT binary patch literal 12221 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 DtsoG7 literal 0 HcmV?d00001 diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 080ef1d81..fb4fa0839 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -2,6 +2,17 @@ The 3.1 release is an intermediate step in the Kickstarter project releases, and includes a range of new functionality. +Some highlights include: + +* A super-smart cursor pagination scheme. +* An improved pagination API, supporting header or in-body pagination styles. +* Pagination controls rendering in the browsable API. +* Better support for API versioning. +* Built-in internalization support. +* Support for Django 1.8's `HStoreField` and `ArrayField`. + +--- + ## Pagination The pagination API has been improved, making it both easier to use, and more powerful. @@ -14,13 +25,13 @@ The cursor based pagination scheme is particularly smart, and is a better approa #### Pagination controls in the browsable API. -Paginated results now include controls that render directly in the browsable API. If you're using the page or limit/offset style, then you'll see a page based control displayed in the browsable API. +Paginated results now include controls that render directly in the browsable API. If you're using the page or limit/offset style, then you'll see a page based control displayed in the browsable API: -**IMAGE** +![page number based pagination](../img/pages-pagination.png ) -The cursor based pagination renders a more simple 'Previous'/'Next' control. +The cursor based pagination renders a more simple style of control: -**IMAGE** +![cursor based pagination](../img/cursor-pagination.png ) #### Support for header-based pagination. @@ -28,6 +39,8 @@ The pagination API was previously only able to alter the pagination style in the For more information, see the [custom pagination styles](../api-guide/pagination/#custom-pagination-styles) documentation. +--- + ## Versioning We've made it easier to build versioned APIs. Built-in schemes for versioning include both URL based and Accept header based variations. @@ -54,6 +67,8 @@ The output representation would match the version used on the incoming request. ] } +--- + ## Internationalization REST framework now includes a built-in set of translations, and supports internationalized error responses. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header. @@ -103,6 +118,8 @@ For more details, see the [internationalization documentation](internationalizat Many thanks to [Craig Blaszczyk](https://github.com/jakul) for helping push this through. +--- + ## New field types Django 1.8's new `ArrayField`, `HStoreField` and `UUIDField` are now all fully supported. @@ -113,12 +130,16 @@ If you're building a new 1.8 project, then you should probably consider using `U http://example.org/api/purchases/9b1a433f-e90d-4948-848b-300fdc26365d +--- + ## ModelSerializer API The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships. For more information, see the documentation on [customizing field mappings](../api-guide/serializers/#customizing-field-mappings) for ModelSerializer classes. +--- + ## Moving packages out of core We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths. @@ -150,7 +171,9 @@ And modify your settings, like so: Thanks go to the latest member of our maintenance team, [José Padilla](https://github.com/jpadilla/), for handling this work and taking on ownership of these packages. -# What's next? +--- + +## What's next? The next focus will be on HTML renderings of API output and will include: diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index 3feff0bad..d998fbeed 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -186,6 +186,10 @@ body{ margin-top: 15px } +#main-content img { + display: block; + margin: 40px auto; +} /* custom navigation styles */ .navbar .navbar-inner{ From 1f996128458570a909d13f15c3d739fb12111984 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 13:21:35 +0000 Subject: [PATCH 174/301] Upgrade pending deprecations to deprecations --- docs/topics/3.1-announcement.md | 10 ++++++++++ rest_framework/request.py | 12 ++++++------ rest_framework/serializers.py | 12 ++++++------ rest_framework/views.py | 2 +- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index fb4fa0839..7242a032a 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -173,6 +173,16 @@ Thanks go to the latest member of our maintenance team, [José Padilla](https:// --- +## Deprecations + +The `request.DATA`, `request.FILES` and `request.QUERY_PARAMS` attributes move from pending deprecation, to deprecated. Use `request.data` and `request.query_params` instead, as discussed in the 3.0 release notes. + +The ModelSerializer Meta options for `write_only_fields`, `view_name` and `lookup_field` are also moved from pending deprecation, to deprecated. Use `extra_kwargs` instead, as discussed in the 3.0 release notes. + +All these attributes and options will still work in 3.1, but their usage will raise a warning. They will be fully removed in 3.2. + +--- + ## What's next? The next focus will be on HTML renderings of API output and will include: diff --git a/rest_framework/request.py b/rest_framework/request.py index bf6ff6706..86fb1ef12 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -219,8 +219,8 @@ class Request(object): Synonym for `.query_params`, for backwards compatibility. """ warnings.warn( - "`request.QUERY_PARAMS` is pending deprecation. Use `request.query_params` instead.", - PendingDeprecationWarning, + "`request.QUERY_PARAMS` is deprecated. Use `request.query_params` instead.", + DeprecationWarning, stacklevel=1 ) return self._request.GET @@ -240,8 +240,8 @@ class Request(object): arbitrary parsers, and also works on methods other than POST (eg PUT). """ warnings.warn( - "`request.DATA` is pending deprecation. Use `request.data` instead.", - PendingDeprecationWarning, + "`request.DATA` is deprecated. Use `request.data` instead.", + DeprecationWarning, stacklevel=1 ) if not _hasattr(self, '_data'): @@ -257,8 +257,8 @@ class Request(object): arbitrary parsers, and also works on methods other than POST (eg PUT). """ warnings.warn( - "`request.FILES` is pending deprecation. Use `request.data` instead.", - PendingDeprecationWarning, + "`request.FILES` is deprecated. Use `request.data` instead.", + DeprecationWarning, stacklevel=1 ) if not _hasattr(self, '_files'): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 188219583..18b810dfe 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1103,9 +1103,9 @@ class ModelSerializer(Serializer): 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. " + "The `Meta.write_only_fields` option is deprecated. " "Use `Meta.extra_kwargs={: {'write_only': True}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) for field_name in write_only_fields: @@ -1116,9 +1116,9 @@ class ModelSerializer(Serializer): view_name = getattr(self.Meta, 'view_name', None) if view_name is not None: warnings.warn( - "The `Meta.view_name` option is pending deprecation. " + "The `Meta.view_name` option is deprecated. " "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) @@ -1128,9 +1128,9 @@ class ModelSerializer(Serializer): lookup_field = getattr(self.Meta, 'lookup_field', None) if lookup_field is not None: warnings.warn( - "The `Meta.lookup_field` option is pending deprecation. " + "The `Meta.lookup_field` option is deprecated. " "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.", - PendingDeprecationWarning, + DeprecationWarning, stacklevel=3 ) kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {}) diff --git a/rest_framework/views.py b/rest_framework/views.py index 9445c840c..b4abc4d95 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -409,7 +409,7 @@ class APIView(View): warnings.warn( 'The `exception_handler(exc)` call signature is deprecated. ' 'Use `exception_handler(exc, context) instead.', - PendingDeprecationWarning + DeprecationWarning ) response = exception_handler(exc) else: From dec3493d7c3ea630a4d51b21bb7e793f0e97ae99 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 14:43:43 +0000 Subject: [PATCH 175/301] Minor cleanup --- rest_framework/fields.py | 19 ++----------------- tests/test_fields.py | 4 ++-- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ecf0dc47b..382fd2dd4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -578,19 +578,10 @@ class CharField(Field): def to_internal_value(self, data): value = six.text_type(data) - - if self.trim_whitespace: - return value.strip() - - return value + return value.strip() if self.trim_whitespace else value def to_representation(self, value): - representation = six.text_type(value) - - if self.trim_whitespace: - return representation.strip() - - return representation + return six.text_type(value) class EmailField(CharField): @@ -603,12 +594,6 @@ class EmailField(CharField): validator = EmailValidator(message=self.error_messages['invalid']) self.validators.append(validator) - def to_internal_value(self, data): - return six.text_type(data).strip() - - def to_representation(self, value): - return six.text_type(value).strip() - class RegexField(CharField): default_error_messages = { diff --git a/tests/test_fields.py b/tests/test_fields.py index 5a5418e65..ab3418bd6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -412,11 +412,11 @@ class TestCharField(FieldValues): def test_trim_whitespace_default(self): field = serializers.CharField() - assert field.to_representation(' abc ') == 'abc' + assert field.to_internal_value(' abc ') == 'abc' def test_trim_whitespace_disabled(self): field = serializers.CharField(trim_whitespace=False) - assert field.to_representation(' abc ') == ' abc ' + assert field.to_internal_value(' abc ') == ' abc ' class TestEmailField(FieldValues): From 5d8c3abe726768d30619f7596fb863b4f83719ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 15:27:35 +0000 Subject: [PATCH 176/301] Fix typo --- docs/api-guide/versioning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index 7463f190b..30dfeb2c0 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -150,7 +150,7 @@ In the following example we're giving a set of views two different possible URL 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. +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 `NamespaceVersioning` is probably easier to manage for larger projects. ## HostNameVersioning From 670723f0216e5aea3aa133c99703949900be3d20 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 15:45:02 +0000 Subject: [PATCH 177/301] Minor cleanups/improvements to ModelSerializer API --- docs/api-guide/serializers.md | 20 ++++++++++++++++++-- rest_framework/serializers.py | 15 +++++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 9a9d5032d..940eb4249 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -577,9 +577,25 @@ Normally if a `ModelSerializer` does not generate the fields you need by default 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_related_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`. +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 `serializers.HyperlinkedRelatedField`. + +### `serializer_url_field` + +The serializer field class that should be used for any `url` field on the serializer. + +Defaults to `serializers.HyperlinkedIdentityField` + +### `serializer_choice_field` + +The serializer field class that should be used for any choice fields on the serializer. + +Defaults to `serializers.ChoiceField` ### The field_class and field_kwargs API diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 18b810dfe..7f3fd078c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -728,7 +728,9 @@ class ModelSerializer(Serializer): models.TimeField: TimeField, models.URLField: URLField, } - serializer_related_class = PrimaryKeyRelatedField + serializer_related_field = PrimaryKeyRelatedField + serializer_url_field = HyperlinkedIdentityField + serializer_choice_field = ChoiceField # Default `create` and `update` behavior... @@ -985,7 +987,7 @@ class ModelSerializer(Serializer): if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. - field_class = ChoiceField + field_class = self.serializer_choice_field if not issubclass(field_class, ModelField): # `model_field` is only valid for the fallback case of @@ -998,11 +1000,12 @@ class ModelSerializer(Serializer): field_kwargs.pop('allow_blank', None) if postgres_fields and isinstance(model_field, postgres_fields.ArrayField): + # Populate the `child` argument on `ListField` instances generated + # for the PostgrSQL specfic `ArrayField`. child_model_field = model_field.base_field child_field_class, child_field_kwargs = self.build_standard_field( 'child', child_model_field ) - field_kwargs['child'] = child_field_class(**child_field_kwargs) return field_class, field_kwargs @@ -1011,7 +1014,7 @@ class ModelSerializer(Serializer): """ Create fields for forward and reverse relationships. """ - field_class = self.serializer_related_class + field_class = self.serializer_related_field field_kwargs = get_relation_kwargs(field_name, relation_info) # `view_name` is only valid for hyperlinked relationships. @@ -1047,7 +1050,7 @@ class ModelSerializer(Serializer): """ Create a field representing the object's own URL. """ - field_class = HyperlinkedIdentityField + field_class = self.serializer_url_field field_kwargs = get_url_kwargs(model_class) return field_class, field_kwargs @@ -1358,7 +1361,7 @@ class HyperlinkedModelSerializer(ModelSerializer): * A 'url' field is included instead of the 'id' field. * Relationships to other instances are hyperlinks, instead of primary keys. """ - serializer_related_class = HyperlinkedRelatedField + serializer_related_field = HyperlinkedRelatedField def get_default_field_names(self, declared_fields, model_info): """ From 0240df1a383342aa6fbb8ea3effa0482ae213d76 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Feb 2015 16:15:10 +0000 Subject: [PATCH 178/301] Minor internal API cleanpu --- rest_framework/serializers.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7f3fd078c..7235d8c51 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -855,8 +855,9 @@ class ModelSerializer(Serializer): ) # Include any kwargs defined in `Meta.extra_kwargs` - field_kwargs = self.build_field_kwargs( - field_kwargs, extra_kwargs, field_name + extra_field_kwargs = extra_kwargs.get(field_name, {}) + field_kwargs = self.include_extra_kwargs( + field_kwargs, extra_field_kwargs ) # Create the serializer field. @@ -1064,14 +1065,12 @@ class ModelSerializer(Serializer): (field_name, model_class.__name__) ) - def build_field_kwargs(self, kwargs, extra_kwargs, field_name): + def include_extra_kwargs(self, kwargs, extra_kwargs): """ - Include an 'extra_kwargs' that have been included for this field, + Include any '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): + if extra_kwargs.get('read_only', False): for attr in [ 'required', 'default', 'allow_blank', 'allow_null', 'min_length', 'max_length', 'min_value', 'max_value', @@ -1079,10 +1078,10 @@ class ModelSerializer(Serializer): ]: kwargs.pop(attr, None) - if extras.get('default') and kwargs.get('required') is False: + if extra_kwargs.get('default') and kwargs.get('required') is False: kwargs.pop('required') - kwargs.update(extras) + kwargs.update(extra_kwargs) return kwargs From 407480b4840990ff17f9a33b293cfcf15bb6f7c5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:01:41 +0000 Subject: [PATCH 179/301] Minor docs work --- docs/api-guide/pagination.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 8ab2edd53..bae579a6d 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -53,12 +53,22 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' } +--- + # API Reference ## PageNumberPagination +**TODO** + ## LimitOffsetPagination +**TODO** + +## CursorPagination + +**TODO** + --- # Custom pagination styles @@ -111,6 +121,12 @@ API responses for list endpoints will now include a `Link` header, instead of in --- +# HTML pagination controls + +## Customizing the controls + +--- + # Third party packages The following third party packages are also available. From d13c807616030b285589cec2fddf4e34a8e22b4a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:02:54 +0000 Subject: [PATCH 180/301] Fix misleading AttributeErrors --- rest_framework/request.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index cfbbdeccd..38fcf9c0a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -18,6 +18,7 @@ from django.utils.six import BytesIO from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.settings import api_settings +import sys import warnings @@ -485,8 +486,16 @@ class Request(object): else: self.auth = None - def __getattr__(self, attr): + def __getattribute__(self, attr): """ - Proxy other attributes to the underlying HttpRequest object. + If an attribute does not exist on this instance, then we also attempt + to proxy it to the underlying HttpRequest object. """ - return getattr(self._request, attr) + try: + return super(Request, self).__getattribute__(attr) + except AttributeError: + info = sys.exc_info() + try: + return getattr(self._request, attr) + except AttributeError: + raise info[0], info[1], info[2].tb_next From 54d82f59ed8a5d2ad4c679680dc52b8a94831d50 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:19:22 +0000 Subject: [PATCH 181/301] Py3 compat fix --- rest_framework/request.py | 8 ++++---- tests/test_request.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 38fcf9c0a..c4de9424a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -12,9 +12,9 @@ from __future__ import unicode_literals from django.conf import settings from django.http import QueryDict from django.http.multipartparser import parse_header +from django.utils import six from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MergeDict as DjangoMergeDict -from django.utils.six import BytesIO from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.settings import api_settings @@ -363,7 +363,7 @@ class Request(object): elif hasattr(self._request, 'read'): self._stream = self._request else: - self._stream = BytesIO(self.raw_post_data) + self._stream = six.BytesIO(self.raw_post_data) def _perform_form_overloading(self): """ @@ -405,7 +405,7 @@ class Request(object): self._CONTENTTYPE_PARAM in self._data ): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) + self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._data, self._files, self._full_data = (Empty, Empty, Empty) def _parse(self): @@ -498,4 +498,4 @@ class Request(object): try: return getattr(self._request, attr) except AttributeError: - raise info[0], info[1], info[2].tb_next + six.reraise(info[0], info[1], info[2].tb_next) diff --git a/tests/test_request.py b/tests/test_request.py index 02a9b1e27..06ad8e937 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -249,6 +249,26 @@ class TestUserSetter(TestCase): login(self.request, self.user) self.assertEqual(self.wrapped_request.user, self.user) + def test_calling_user_fails_when_attribute_error_is_raised(self): + """ + This proves that when an AttributeError is raised inside of the request.user + property, that we can handle this and report the true, underlying error. + """ + class AuthRaisesAttributeError(object): + def authenticate(self, request): + import rest_framework + rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST + + self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),)) + SessionMiddleware().process_request(self.request) + + login(self.request, self.user) + try: + self.request.user + except AttributeError as error: + self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'") + else: + assert False, 'AttributeError not raised' class TestAuthSetter(TestCase): From 0669f507b3a63114cf429f0662b1781f0e1fa5f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:22:13 +0000 Subject: [PATCH 182/301] pep8 fix --- tests/test_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request.py b/tests/test_request.py index 06ad8e937..c274ab69d 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -270,8 +270,8 @@ class TestUserSetter(TestCase): else: assert False, 'AttributeError not raised' -class TestAuthSetter(TestCase): +class TestAuthSetter(TestCase): def test_auth_can_be_set(self): request = Request(factory.get('/')) request.auth = 'DUMMY' From b2939c157d32e604e10099be891e382d8c54bbec Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:43:20 +0000 Subject: [PATCH 183/301] Fixes for latest version of pep8 --- env/pip-selfcheck.json | 1 + rest_framework/templatetags/rest_framework.py | 4 +++- tests/test_authentication.py | 5 ++++- tests/test_relations_hyperlink.py | 4 +++- tests/test_renderers.py | 9 +++++++-- tests/test_response.py | 9 +++++++-- tests/test_throttling.py | 8 ++++++-- 7 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 env/pip-selfcheck.json diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json new file mode 100644 index 000000000..db1087af9 --- /dev/null +++ b/env/pip-selfcheck.json @@ -0,0 +1 @@ +{"last_check":"2015-02-09T17:34:33Z","pypi_version":"6.0.8"} \ No newline at end of file diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 69e03af40..d66ffb330 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -154,7 +154,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru If autoescape is True, the link text and URLs will get autoescaped. """ - trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + def trim_url(x, limit=trim_url_limit): + return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) words = word_split_re.split(force_text(text)) for i, word in enumerate(words): diff --git a/tests/test_authentication.py b/tests/test_authentication.py index caabcc214..19fe6043f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -205,7 +205,10 @@ class TokenAuthTests(TestCase): def test_post_json_makes_one_db_query(self): """Ensure that authenticating a user using a token performs only one DB query""" auth = "Token " + self.key - func_to_test = lambda: self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + + def func_to_test(): + return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + self.assertNumQueries(1, func_to_test) def test_post_form_failing_token_auth(self): diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py index f1b882edf..2230c275c 100644 --- a/tests/test_relations_hyperlink.py +++ b/tests/test_relations_hyperlink.py @@ -12,7 +12,9 @@ factory = APIRequestFactory() request = factory.get('/') # Just to ensure we have a request in the serializer context -dummy_view = lambda request, pk: None +def dummy_view(request, pk): + pass + urlpatterns = patterns( '', diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 54eea8ceb..4f41144e5 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -28,8 +28,13 @@ import re DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): + return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): + return ('Renderer B: %s' % x).encode('ascii') expected_results = [ diff --git a/tests/test_response.py b/tests/test_response.py index f233ae332..4a9deaa29 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -38,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer): DUMMYSTATUS = status.HTTP_200_OK DUMMYCONTENT = 'dummycontent' -RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii') -RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii') + +def RENDERER_A_SERIALIZER(x): + return ('Renderer A: %s' % x).encode('ascii') + + +def RENDERER_B_SERIALIZER(x): + return ('Renderer B: %s' % x).encode('ascii') class RendererA(BaseRenderer): diff --git a/tests/test_throttling.py b/tests/test_throttling.py index cc36a004c..50a53b3eb 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -188,7 +188,9 @@ class ScopedRateThrottleTests(TestCase): class XYScopedRateThrottle(ScopedRateThrottle): TIMER_SECONDS = 0 THROTTLE_RATES = {'x': '3/min', 'y': '1/min'} - timer = lambda self: self.TIMER_SECONDS + + def timer(self): + return self.TIMER_SECONDS class XView(APIView): throttle_classes = (XYScopedRateThrottle,) @@ -290,7 +292,9 @@ class XffTestingBase(TestCase): class Throttle(ScopedRateThrottle): THROTTLE_RATES = {'test_limit': '1/day'} TIMER_SECONDS = 0 - timer = lambda self: self.TIMER_SECONDS + + def timer(self): + return self.TIMER_SECONDS class View(APIView): throttle_classes = (Throttle,) From 1a087c8c5bac6f157979ef9ff540c0eb23848fb4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:47:59 +0000 Subject: [PATCH 184/301] Fix .gitignore --- .gitignore | 18 +++++++----------- env/pip-selfcheck.json | 1 - 2 files changed, 7 insertions(+), 12 deletions(-) delete mode 100644 env/pip-selfcheck.json diff --git a/.gitignore b/.gitignore index 2bdf8f7eb..3d5f1043d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,18 +3,14 @@ *~ .* -site/ -htmlcov/ -coverage/ -build/ -dist/ -*.egg-info/ +/site/ +/htmlcov/ +/coverage/ +/build/ +/dist/ +/*.egg-info/ +/env/ MANIFEST -bin/ -include/ -lib/ -local/ - !.gitignore !.travis.yml diff --git a/env/pip-selfcheck.json b/env/pip-selfcheck.json deleted file mode 100644 index db1087af9..000000000 --- a/env/pip-selfcheck.json +++ /dev/null @@ -1 +0,0 @@ -{"last_check":"2015-02-09T17:34:33Z","pypi_version":"6.0.8"} \ No newline at end of file From 7b639c0cd0676172cc8502e833f5b708f39f9a83 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Feb 2015 17:57:08 +0000 Subject: [PATCH 185/301] Drop django master from the build matrix. Currently always going to be a know failure case. We can add it back when we start to consdier Django 1.9 support. --- .travis.yml | 12 ------------ tox.ini | 3 +-- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28ebfc00f..4f9297853 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,18 +25,6 @@ env: - TOX_ENV=py33-django18alpha - TOX_ENV=py32-django18alpha - TOX_ENV=py27-django18alpha - - TOX_ENV=py34-djangomaster - - TOX_ENV=py33-djangomaster - - TOX_ENV=py32-djangomaster - - TOX_ENV=py27-djangomaster - -matrix: - fast_finish: true - allow_failures: - - env: TOX_ENV=py34-djangomaster - - env: TOX_ENV=py33-djangomaster - - env: TOX_ENV=py32-djangomaster - - env: TOX_ENV=py27-djangomaster install: - pip install tox diff --git a/tox.ini b/tox.ini index 8e0369643..eda92c19b 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27-{flake8,docs}, {py26,py27}-django14, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18alpha,master} + {py27,py32,py33,py34}-django{17,18alpha} [testenv] commands = ./runtests.py --fast @@ -15,7 +15,6 @@ deps = django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ - djangomaster: https://github.com/django/django/zipball/master {py26,py27}-django{14,15,16,17}: django-guardian==1.2.3 {py26,py27}-django{14,15,16}: oauth2==1.5.211 {py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1 From b18d773b517a93528fbdba043b9da8d7bc2cf5b4 Mon Sep 17 00:00:00 2001 From: Joar Leth Date: Mon, 9 Feb 2015 22:51:49 +0100 Subject: [PATCH 186/301] Fix typo in 3.1 announcement --- docs/topics/3.1-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 7242a032a..f500101c5 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -8,7 +8,7 @@ Some highlights include: * An improved pagination API, supporting header or in-body pagination styles. * Pagination controls rendering in the browsable API. * Better support for API versioning. -* Built-in internalization support. +* Built-in internationalization support. * Support for Django 1.8's `HStoreField` and `ArrayField`. --- From d87bb67d11918683425af1c1d56c0c57f50e81b3 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Feb 2015 10:50:35 +0100 Subject: [PATCH 187/301] Failing test case for #1488 --- tests/test_filters.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index 355f02cef..e7cb0c795 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -429,6 +429,56 @@ class SearchFilterTests(TestCase): reload_module(filters) +class AttributeModel(models.Model): + label = models.CharField(max_length=32) + + +class SearchFilterModelM2M(models.Model): + title = models.CharField(max_length=20) + text = models.CharField(max_length=100) + attributes = models.ManyToManyField(AttributeModel) + + +class SearchFilterM2MSerializer(serializers.ModelSerializer): + class Meta: + model = SearchFilterModelM2M + + +class SearchFilterM2MTests(TestCase): + def setUp(self): + # Sequence of title/text/attributes is: + # + # z abc [1, 2, 3] + # zz bcd [1, 2, 3] + # zzz cde [1, 2, 3] + # ... + for idx in range(3): + label = 'w' * (idx + 1) + AttributeModel(label=label) + + for idx in range(10): + title = 'z' * (idx + 1) + text = ( + chr(idx + ord('a')) + + chr(idx + ord('b')) + + chr(idx + ord('c')) + ) + SearchFilterModelM2M(title=title, text=text).save() + SearchFilterModelM2M.objects.get(title='zz').attributes.add(1, 2, 3) + + def test_m2m_search(self): + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModelM2M.objects.all() + serializer_class = SearchFilterM2MSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('=title', 'text', 'attributes__label') + + view = SearchListView.as_view() + request = factory.get('/', {'search': 'zz'}) + response = view(request) + self.assertEqual(len(response.data), 1) + + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20) text = models.CharField(max_length=100) From 3522b69394d932c8bf8028a456b6d9b64c38b54e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Feb 2015 10:51:38 +0100 Subject: [PATCH 188/301] Add `distinct` call in `filter_queryset` --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index d188a2d1e..d3f55a447 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -104,7 +104,7 @@ class SearchFilter(BaseFilterBackend): for search_term in self.get_search_terms(request): or_queries = [models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups] - queryset = queryset.filter(reduce(operator.or_, or_queries)) + queryset = queryset.filter(reduce(operator.or_, or_queries)).distinct() return queryset From f6033cee87e367ec3a6ffcdd6897656b3e3c0493 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 10 Feb 2015 22:36:41 +0100 Subject: [PATCH 189/301] Add release notes for 3.0.5. --- docs/topics/release-notes.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e0894d2d9..e74dc803f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -41,6 +41,19 @@ You can determine your currently installed version using `pip freeze`: ## 3.0.x series +### 3.0.5 + +**Date**: [10th February 2015][3.0.5-milestone]. + +* Fix a bug where `_closable_objects` breaks pickling. ([#1850][gh1850], [#2492][gh2492]) +* Allow non-standard `User` models with `Throttling`. ([#2524][gh2524]) +* Support custom `User.db_table` in TokenAuthentication migration. ([#2479][gh2479]) +* Fix misleading `AttributeError` tracebacks on `Request` objects. ([#2530][gh2530], [#2108][gh2108]) +* `ManyRelatedField.get_value` clearing field on partial update. ([#2475][gh2475]) +* Removed '.model' shortcut from code. ([#2486][gh2486]) +* Fix `detail_route` and `list_route` mutable argument. ([#2518][gh2518]) +* Prefetching the user object when getting the token in `TokenAuthentication`. ([#2519][gh2519]) + ### 3.0.4 **Date**: [28th January 2015][3.0.4-milestone]. @@ -721,6 +734,7 @@ For older release notes, [please see the GitHub repo](old-release-notes). [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22 [3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22 +[3.0.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -808,3 +822,14 @@ For older release notes, [please see the GitHub repo](old-release-notes). [gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399 [gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388 [gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360 + +[gh1850]: https://github.com/tomchristie/django-rest-framework/issues/1850 +[gh2108]: https://github.com/tomchristie/django-rest-framework/issues/2108 +[gh2475]: https://github.com/tomchristie/django-rest-framework/issues/2475 +[gh2479]: https://github.com/tomchristie/django-rest-framework/issues/2479 +[gh2486]: https://github.com/tomchristie/django-rest-framework/issues/2486 +[gh2492]: https://github.com/tomchristie/django-rest-framework/issues/2492 +[gh2518]: https://github.com/tomchristie/django-rest-framework/issues/2518 +[gh2519]: https://github.com/tomchristie/django-rest-framework/issues/2519 +[gh2524]: https://github.com/tomchristie/django-rest-framework/issues/2524 +[gh2530]: https://github.com/tomchristie/django-rest-framework/issues/2530 From 59b3fe8f395b2d6ee4091df81f9dbbc7e47cf84e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 10 Feb 2015 22:48:04 +0100 Subject: [PATCH 190/301] Bumped the version to 3.0.5 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 57e5421b8..9b58f09f4 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.0.4' +__version__ = '3.0.5' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' From c3425accdedb64e99238619f8f740bb547b2ecef Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 11 Feb 2015 14:19:07 +0000 Subject: [PATCH 191/301] Fix incorrect HTML parsing for DictField --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 382fd2dd4..a5348922a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1192,9 +1192,9 @@ class DictField(Field): def get_value(self, dictionary): # We override the default field access in order to support - # lists in HTML forms. + # dictionaries in HTML forms. if html.is_html_input(dictionary): - return html.parse_html_list(dictionary, prefix=self.field_name) + return html.parse_html_dict(dictionary, prefix=self.field_name) return dictionary.get(self.field_name, empty) def to_internal_value(self, data): From 9d80335ac86076f75c81de02abc0cda8f98916d7 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Thu, 12 Feb 2015 01:10:03 +0100 Subject: [PATCH 192/301] Remove '.model' shortcut from viewset docs. Refs #2486. Closes #2549. --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index bbf92c6ce..4fd7aa84c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -178,7 +178,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, #### Example -Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: class AccountViewSet(viewsets.ModelViewSet): """ From daf1d59d0f41d2ea89e0b996d22b5d4e84914fb5 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 17 Feb 2015 11:22:37 +0100 Subject: [PATCH 193/301] Adjust importlib import --- rest_framework/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 7331f2655..8ccfd3ed0 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -20,7 +20,11 @@ back to the defaults. from __future__ import unicode_literals from django.test.signals import setting_changed from django.conf import settings -from django.utils import importlib, six +try: + import importlib +except ImportError: + from django.utils import importlib +from django.utils import six from rest_framework import ISO_8601 From c5eb5b22018e55bffe080bb3f14e34ab6b493073 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 17 Feb 2015 11:55:15 +0100 Subject: [PATCH 194/301] Move `importlib` fallback into compat. --- rest_framework/compat.py | 5 ++++- rest_framework/settings.py | 6 +----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 50f370143..c6a4a8698 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -12,7 +12,10 @@ from django.utils.six.moves.urllib.parse import urlparse as _urlparse from django.utils import six import django import inspect - +try: + import importlib +except ImportError: + from django.utils import importlib def unicode_repr(instance): # Get the repr of an instance, but ensure it is a unicode string diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8ccfd3ed0..394b12622 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -20,13 +20,9 @@ back to the defaults. from __future__ import unicode_literals from django.test.signals import setting_changed from django.conf import settings -try: - import importlib -except ImportError: - from django.utils import importlib from django.utils import six from rest_framework import ISO_8601 - +from rest_framework.compat import importlib USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None) From dbd23521656b366cbaa1382a0d222f8fe4e3a326 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 10:58:00 +0000 Subject: [PATCH 195/301] Fixes for latest pep8 updates. Refs #2563. --- rest_framework/renderers.py | 8 ++++---- rest_framework/request.py | 6 +++--- rest_framework/serializers.py | 14 ++++++++------ rest_framework/templatetags/rest_framework.py | 4 ++-- rest_framework/utils/model_meta.py | 4 ++-- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6256acdd7..339bd0d17 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -458,8 +458,8 @@ class BrowsableAPIRenderer(BaseRenderer): return True # Don't actually need to return a form if ( - not getattr(view, 'get_serializer', None) - or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes) + not getattr(view, 'get_serializer', None) or + not any(is_form_media_type(parser.media_type) for parser in view.parser_classes) ): return @@ -503,8 +503,8 @@ class BrowsableAPIRenderer(BaseRenderer): # If we're not using content overloading there's no point in # supplying a generic form, as the view won't treat the form's # value as the content of the request. - if not (api_settings.FORM_CONTENT_OVERRIDE - and api_settings.FORM_CONTENTTYPE_OVERRIDE): + if not (api_settings.FORM_CONTENT_OVERRIDE and + api_settings.FORM_CONTENTTYPE_OVERRIDE): return None # Check permissions diff --git a/rest_framework/request.py b/rest_framework/request.py index 081ace236..fd4f6a3e2 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -383,9 +383,9 @@ class Request(object): # We only need to use form overloading on form POST requests. if ( - not USE_FORM_OVERLOADING - or self._request.method != 'POST' - or not is_form_media_type(self._content_type) + self._request.method != 'POST' or + not USE_FORM_OVERLOADING or + not is_form_media_type(self._content_type) ): return diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c60574d4f..9475e119b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -336,8 +336,8 @@ class Serializer(BaseSerializer): return OrderedDict([ (field_name, field.get_value(self.initial_data)) for field_name, field in self.fields.items() - if field.get_value(self.initial_data) is not empty - and not field.read_only + if (field.get_value(self.initial_data) is not empty) and + not field.read_only ]) return OrderedDict([ @@ -653,8 +653,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # ... # profile = ProfileSerializer() assert not any( - isinstance(field, BaseSerializer) and (key in validated_data) - and isinstance(validated_data[key], (list, dict)) + isinstance(field, BaseSerializer) and + (key in validated_data) and + isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable nested' @@ -673,8 +674,9 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # ... # address = serializer.CharField('profile.address') assert not any( - '.' in field.source and (key in validated_data) - and isinstance(validated_data[key], (list, dict)) + '.' in field.source and + (key in validated_data) and + isinstance(validated_data[key], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable dotted-source ' diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 699ea897f..bf0dc7b8f 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -162,8 +162,8 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru lead = lead + opening # Keep parentheses at the end only if they're balanced. if ( - middle.endswith(closing) - and middle.count(closing) == middle.count(opening) + 1 + middle.endswith(closing) and + middle.count(closing) == middle.count(opening) + 1 ): middle = middle[:-len(closing)] trail = closing + trail diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index dd92f8b67..d92bceb98 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -145,8 +145,8 @@ def _get_reverse_relationships(opts): related_model=related, to_many=True, has_through_model=( - (getattr(relation.field.rel, 'through', None) is not None) - and not relation.field.rel.through._meta.auto_created + (getattr(relation.field.rel, 'through', None) is not None) and + not relation.field.rel.through._meta.auto_created ) ) From 9f4f9c9c9f09f46776e75435f4fe727789721e81 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 12:46:55 +0000 Subject: [PATCH 196/301] Single source of truth for requirements --- requirements.txt | 30 ++++++++------------- requirements/requirements-codestyle.txt | 3 +++ requirements/requirements-documentation.txt | 2 ++ requirements/requirements-optionals.txt | 4 +++ requirements/requirements-packaging.txt | 11 ++++++++ requirements/requirements-testing.txt | 3 +++ tox.ini | 13 +++++---- 7 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 requirements/requirements-codestyle.txt create mode 100644 requirements/requirements-documentation.txt create mode 100644 requirements/requirements-optionals.txt create mode 100644 requirements/requirements-packaging.txt create mode 100644 requirements/requirements-testing.txt diff --git a/requirements.txt b/requirements.txt index bf4611792..4ec84f684 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,13 @@ -# Minimum Django version -Django>=1.4.11 +# The base set of requirements for REST framework is actually +# just Django, but for the purposes of development and testing +# there are a number of packages that it is useful to install. -# Test requirements -pytest-django==2.8.0 -pytest==2.6.4 -pytest-cov==1.6 -flake8==2.2.2 +# Laying these out as seperate requirements files, allows us to +# only included the relevent sets when running tox, and ensures +# we are only ever declaring out dependancies in one place. -# Optional packages -markdown>=2.1.0 -django-guardian==1.2.4 -django-filter>=0.9.2 - -# wheel for PyPI installs -wheel==0.24.0 -# twine for secured PyPI uploads -twine==1.4.0 - -# MkDocs for documentation previews/deploys -mkdocs==0.11.1 +-r requirements/requirements-optionals.txt +-r requirements/requirements-testing.txt +-r requirements/requirements-documentation.txt +-r requirements/requirements-codestyle.txt +-r requirements/requirements-packaging.txt diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt new file mode 100644 index 000000000..4e2be24c3 --- /dev/null +++ b/requirements/requirements-codestyle.txt @@ -0,0 +1,3 @@ +# PEP8 code linting, which we run on all commits. +flake8==2.3.0 +pep8==1.6.2 diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt new file mode 100644 index 000000000..5009436e4 --- /dev/null +++ b/requirements/requirements-documentation.txt @@ -0,0 +1,2 @@ +# MkDocs to build our documentation. +mkdocs==0.11.1 diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt new file mode 100644 index 000000000..af9937cfa --- /dev/null +++ b/requirements/requirements-optionals.txt @@ -0,0 +1,4 @@ +# Optional packages which may be used with REST framework. +markdown==2.5.2 +django-guardian==1.2.5 +django-filter==0.9.2 diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt new file mode 100644 index 000000000..e4ac77403 --- /dev/null +++ b/requirements/requirements-packaging.txt @@ -0,0 +1,11 @@ +# Wheel for PyPI installs. +wheel==0.24.0 + +# Twine for secured PyPI uploads. +twine==1.4.0 + +# Transifex client for managing translation resources. +transifex-client==0.10 + +# The pip-review and pip-dump tools for package upgrades. +pip-tools==0.3.5 diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt new file mode 100644 index 000000000..a8d5d3229 --- /dev/null +++ b/requirements/requirements-testing.txt @@ -0,0 +1,3 @@ +# PyTest for running the tests. +pytest==2.6.4 +pytest-django==2.8.0 diff --git a/tox.ini b/tox.ini index 76f4f09b3..b96b4939b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,18 +15,17 @@ deps = django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ - django-guardian==1.2.4 - pytest-django==2.8.0 - django-filter==0.9.2 - markdown>=2.1.0 + -rrequirements/requirements-testing.txt + -rrequirements/requirements-optionals.txt [testenv:py27-flake8] deps = - pytest==2.6.4 - flake8==2.2.2 + -rrequirements/requirements-testing.txt + -rrequirements/requirements-codestyle.txt commands = ./runtests.py --lintonly [testenv:py27-docs] deps = - mkdocs>=0.11.1 + -rrequirements/requirements-testing.txt + -rrequirements/requirements-documentation.txt commands = mkdocs build From 028c477c2242e7c322b68c4730ed1868008c37d8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 12:56:19 +0000 Subject: [PATCH 197/301] Note on using pip-review in docs. --- docs/topics/project-management.md | 10 ++++++++++ requirements/requirements-packaging.txt | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md index 2a54fb94e..dfe0d6357 100644 --- a/docs/topics/project-management.md +++ b/docs/topics/project-management.md @@ -166,6 +166,16 @@ When a translator has finished translating their work needs to be downloaded fro --- +## Project requirements + +All our test requirements are pinned to exact versions, in order to ensure that our test runs are reproducible. We maintain the requirements in the `requirements` directory. The requirements files are referenced from the `tox.ini` configuration file, ensuring we have a single source of truth for package versions used in testing. + +You can check if there are any packages available at a newer version, by using the `pip-review` tool. + +Package upgrades should generally be treated as isolated pull requests. Also note that the `pip-dump` command does not work gracefully with our requirements layout style, so any edits should be made manually. + +--- + ## Project ownership The PyPI package is owned by `@tomchristie`. As a backup `@j4mie` also has ownership of the package. diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index e4ac77403..7782d63a7 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -7,5 +7,5 @@ twine==1.4.0 # Transifex client for managing translation resources. transifex-client==0.10 -# The pip-review and pip-dump tools for package upgrades. +# The pip-review tool for checking package upgrades. pip-tools==0.3.5 From 691ae5b646ab1ef7f965ffc239777134153948b6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 13:40:59 +0000 Subject: [PATCH 198/301] Recommend 'pip list --outdated' instead of using pip-review --- docs/topics/project-management.md | 2 +- requirements/requirements-packaging.txt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md index dfe0d6357..c9ecc1ed5 100644 --- a/docs/topics/project-management.md +++ b/docs/topics/project-management.md @@ -170,7 +170,7 @@ When a translator has finished translating their work needs to be downloaded fro All our test requirements are pinned to exact versions, in order to ensure that our test runs are reproducible. We maintain the requirements in the `requirements` directory. The requirements files are referenced from the `tox.ini` configuration file, ensuring we have a single source of truth for package versions used in testing. -You can check if there are any packages available at a newer version, by using the `pip-review` tool. +You can check if there are any packages available at a newer version, by using the `pip list --outdated`. Package upgrades should generally be treated as isolated pull requests. Also note that the `pip-dump` command does not work gracefully with our requirements layout style, so any edits should be made manually. diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index 7782d63a7..1efb2f836 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -6,6 +6,3 @@ twine==1.4.0 # Transifex client for managing translation resources. transifex-client==0.10 - -# The pip-review tool for checking package upgrades. -pip-tools==0.3.5 From f1e517449a68d2d67181781bc070d525ad2e3815 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 13:41:59 +0000 Subject: [PATCH 199/301] Minor docs tweak --- docs/topics/project-management.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md index c9ecc1ed5..4926f3554 100644 --- a/docs/topics/project-management.md +++ b/docs/topics/project-management.md @@ -170,9 +170,7 @@ When a translator has finished translating their work needs to be downloaded fro All our test requirements are pinned to exact versions, in order to ensure that our test runs are reproducible. We maintain the requirements in the `requirements` directory. The requirements files are referenced from the `tox.ini` configuration file, ensuring we have a single source of truth for package versions used in testing. -You can check if there are any packages available at a newer version, by using the `pip list --outdated`. - -Package upgrades should generally be treated as isolated pull requests. Also note that the `pip-dump` command does not work gracefully with our requirements layout style, so any edits should be made manually. +Package upgrades should generally be treated as isolated pull requests. You can check if there are any packages available at a newer version, by using the `pip list --outdated`. --- From e45e0f056782cf6406a106add5c11803dcb24e30 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 17 Feb 2015 13:44:42 +0000 Subject: [PATCH 200/301] Update version --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 9b58f09f4..f8bbeee36 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.0.5' +__version__ = '3.1.0' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' From 30e6f32f6fbe20eafe949017cd62aed5d15529d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yannick=20P=C3=89ROUX?= Date: Tue, 17 Feb 2015 17:08:30 +0100 Subject: [PATCH 201/301] Fix typo in requests.md --- docs/api-guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 77000ffa2..c993dfae5 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -38,7 +38,7 @@ For clarity inside your code, we recommend using `request.query_params` instead ## .DATA and .FILES -The old-style version 2.x `request.data` and `request.FILES` attributes are still available, but are now pending deprecation in favor of the unified `request.data` attribute. +The old-style version 2.x `request.DATA` and `request.FILES` attributes are still available, but are now pending deprecation in favor of the unified `request.data` attribute. ## .QUERY_PARAMS From d9c652813d3ab0ddebbb5503ce8ed57c49e1d0d2 Mon Sep 17 00:00:00 2001 From: Paul Hallett Date: Mon, 16 Feb 2015 16:07:08 +0000 Subject: [PATCH 202/301] add missing import in tests --- docs/api-guide/testing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index cd8c7820a..d9a1696dd 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -65,6 +65,8 @@ When testing views directly using a request factory, it's often convenient to be To forcibly authenticate a request, use the `force_authenticate()` method. + from rest_framework.tests import force_authenticate + factory = APIRequestFactory() user = User.objects.get(username='olivia') view = AccountDetail.as_view() From 3d85473edf847ba64aa499b336ca21f6b3d3c6b8 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Wed, 18 Feb 2015 21:00:12 +0300 Subject: [PATCH 203/301] Fix UniqueTogetherValidator for NULL values --- rest_framework/validators.py | 4 +++- tests/test_validators.py | 20 +++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index e3719b8d5..c030abdba 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -138,7 +138,9 @@ class UniqueTogetherValidator: queryset = self.queryset queryset = self.filter_queryset(attrs, queryset) queryset = self.exclude_current_instance(attrs, queryset) - if queryset.exists(): + + # Ignore validation if any field is None + if None not in attrs.values() and queryset.exists(): field_names = ', '.join(self.fields) raise ValidationError(self.message.format(field_names=field_names)) diff --git a/tests/test_validators.py b/tests/test_validators.py index 072cec360..185febf83 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -76,8 +76,8 @@ class TestUniquenessValidation(TestCase): # ----------------------------------- class UniquenessTogetherModel(models.Model): - race_name = models.CharField(max_length=100) - position = models.IntegerField() + race_name = models.CharField(max_length=100, null=True) + position = models.IntegerField(null=True) class Meta: unique_together = ('race_name', 'position') @@ -108,8 +108,8 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" UniquenessTogetherSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(max_length=100, required=True) - position = IntegerField(required=True) + race_name = CharField(allow_null=True, max_length=100, required=True) + position = IntegerField(allow_null=True, required=True) class Meta: validators = [] """) @@ -178,10 +178,20 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" ExcludedFieldSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(max_length=100) + race_name = CharField(allow_null=True, max_length=100, required=False) """) assert repr(serializer) == expected + def test_ignore_validation_for_null_fields(self): + UniquenessTogetherModel.objects.create( + race_name=None, + position=None + ) + data = {'race_name': None, 'position': None} + serializer = UniquenessTogetherSerializer(data=data) + + assert serializer.is_valid() + # Tests for `UniqueForDateValidator` # ---------------------------------- From fe8d95f93e11d801d07c8852b12abb4f6b21e1e6 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Thu, 19 Feb 2015 18:03:44 +0300 Subject: [PATCH 204/301] Skip validation of NULL field only if it part of unique_together --- rest_framework/validators.py | 5 +++- tests/test_validators.py | 54 ++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index c030abdba..ab3616149 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -140,7 +140,10 @@ class UniqueTogetherValidator: queryset = self.exclude_current_instance(attrs, queryset) # Ignore validation if any field is None - if None not in attrs.values() and queryset.exists(): + checked_values = [ + value for field, value in attrs.items() if field in self.fields + ] + if None not in checked_values and queryset.exists(): field_names = ', '.join(self.fields) raise ValidationError(self.message.format(field_names=field_names)) diff --git a/tests/test_validators.py b/tests/test_validators.py index 185febf83..c4c60b7fe 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -83,11 +83,37 @@ class UniquenessTogetherModel(models.Model): unique_together = ('race_name', 'position') +class NullUniquenessTogetherModel(models.Model): + """ + Used to ensure that null values are not included when checking + unique_together constraints. + + Ignoring items which have a null in any of the validated fields is the same + behavior that database backends will use when they have the + unique_together constraint added. + + Example case: a null position could indicate a non-finisher in the race, + there could be many non-finishers in a race, but all non-NULL + values *should* be unique against the given `race_name`. + """ + date_of_birth = models.DateField(null=True) # Not part of the uniqueness constraint + race_name = models.CharField(max_length=100) + position = models.IntegerField(null=True) + + class Meta: + unique_together = ('race_name', 'position') + + class UniquenessTogetherSerializer(serializers.ModelSerializer): class Meta: model = UniquenessTogetherModel +class NullUniquenessTogetherSerializer(serializers.ModelSerializer): + class Meta: + model = NullUniquenessTogetherModel + + class TestUniquenessTogetherValidation(TestCase): def setUp(self): self.instance = UniquenessTogetherModel.objects.create( @@ -183,15 +209,33 @@ class TestUniquenessTogetherValidation(TestCase): assert repr(serializer) == expected def test_ignore_validation_for_null_fields(self): - UniquenessTogetherModel.objects.create( - race_name=None, + # None values that are on fields which are part of the uniqueness + # constraint cause the instance to ignore uniqueness validation. + NullUniquenessTogetherModel.objects.create( + date_of_birth=datetime.date(2000, 1, 1), + race_name='Paris Marathon', position=None ) - data = {'race_name': None, 'position': None} - serializer = UniquenessTogetherSerializer(data=data) - + data = { + 'date': datetime.date(2000, 1, 1), + 'race_name': 'Paris Marathon', + 'position': None + } + serializer = NullUniquenessTogetherSerializer(data=data) assert serializer.is_valid() + def test_do_not_ignore_validation_for_null_fields(self): + # None values that are not on fields part of the uniqueness constraint + # do not cause the instance to skip validation. + NullUniquenessTogetherModel.objects.create( + date_of_birth=datetime.date(2000, 1, 1), + race_name='Paris Marathon', + position=1 + ) + data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1} + serializer = NullUniquenessTogetherSerializer(data=data) + assert not serializer.is_valid() + # Tests for `UniqueForDateValidator` # ---------------------------------- From aa7ed316d842c06d7eb6907d4481d72c747991d7 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Thu, 19 Feb 2015 18:09:04 +0300 Subject: [PATCH 205/301] Return UniquenessTogetherModel to previous state --- tests/test_validators.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index c4c60b7fe..127ec6f8b 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -76,8 +76,8 @@ class TestUniquenessValidation(TestCase): # ----------------------------------- class UniquenessTogetherModel(models.Model): - race_name = models.CharField(max_length=100, null=True) - position = models.IntegerField(null=True) + race_name = models.CharField(max_length=100) + position = models.IntegerField() class Meta: unique_together = ('race_name', 'position') @@ -134,8 +134,8 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" UniquenessTogetherSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(allow_null=True, max_length=100, required=True) - position = IntegerField(allow_null=True, required=True) + race_name = CharField(max_length=100, required=True) + position = IntegerField(required=True) class Meta: validators = [] """) @@ -204,7 +204,7 @@ class TestUniquenessTogetherValidation(TestCase): expected = dedent(""" ExcludedFieldSerializer(): id = IntegerField(label='ID', read_only=True) - race_name = CharField(allow_null=True, max_length=100, required=False) + race_name = CharField(max_length=100) """) assert repr(serializer) == expected From 777f4e8212e76f63f474d5d7e089de69dda022a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 19 Feb 2015 12:23:44 -0400 Subject: [PATCH 206/301] Failing test for #2552 --- tests/test_renderers.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 60a082250..cb76f6830 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -10,7 +10,10 @@ from rest_framework import status, permissions from rest_framework.compat import OrderedDict from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.renderers import BaseRenderer, JSONRenderer, BrowsableAPIRenderer +from rest_framework import serializers +from rest_framework.renderers import ( + BaseRenderer, JSONRenderer, BrowsableAPIRenderer, HTMLFormRenderer +) from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from collections import MutableMapping @@ -455,3 +458,16 @@ class TestJSONIndentationStyles: renderer.compact = False data = OrderedDict([('a', 1), ('b', 2)]) assert renderer.render(data) == b'{"a": 1, "b": 2}' + + +class TestHiddenFieldHTMLFormRenderer(TestCase): + def test_hidden_field_rendering(self): + class TestSerializer(serializers.Serializer): + published = serializers.HiddenField(default=True) + + serializer = TestSerializer(data={}) + serializer.is_valid() + renderer = HTMLFormRenderer() + field = serializer['published'] + rendered = renderer.render_field(field, {}) + assert rendered == '' From 60617f876a78b5b81b47a63f9711c4b9eac03a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Dur=C3=A1=20Tar=C3=AD?= Date: Thu, 12 Feb 2015 12:03:00 +0000 Subject: [PATCH 207/301] Fixes HiddenField being rendered in HTMLFormRenderer --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 339bd0d17..920d2bc47 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -305,7 +305,7 @@ class HTMLFormRenderer(BaseRenderer): }) def render_field(self, field, parent_style): - if isinstance(field, serializers.HiddenField): + if isinstance(field._field, serializers.HiddenField): return '' style = dict(self.default_style[field]) From c0916c2859468f4888d688217baca73747fd3bf7 Mon Sep 17 00:00:00 2001 From: aRkadeFR Date: Fri, 20 Feb 2015 15:59:10 +0100 Subject: [PATCH 208/301] Documentation test fix double word --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index d9a1696dd..1b96b325e 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. ## Creating test requests -The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. +The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. from rest_framework.test import APIRequestFactory From c8609ba652e1752e690c9e27e02b3531589d0c2c Mon Sep 17 00:00:00 2001 From: Rense VanderHoek Date: Fri, 20 Feb 2015 16:31:12 +0100 Subject: [PATCH 209/301] Set field length/values as actual attributes. The SimpleMetadata class in metadata.py tries to getattr() attributes on a field. For this to work, max_length and min_length have to be actually set as an attribute. Did the same for min_value and max_value and added those two to SimpleMetadata.get_field_info --- rest_framework/fields.py | 58 ++++++++++++++++++++------------------ rest_framework/metadata.py | 8 +++++- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a5348922a..561ec93c2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -556,15 +556,15 @@ class CharField(Field): def __init__(self, **kwargs): self.allow_blank = kwargs.pop('allow_blank', False) self.trim_whitespace = kwargs.pop('trim_whitespace', True) - max_length = kwargs.pop('max_length', None) - min_length = kwargs.pop('min_length', None) + self.max_length = kwargs.pop('max_length', None) + self.min_length = kwargs.pop('min_length', None) super(CharField, self).__init__(**kwargs) - if max_length is not None: - message = self.error_messages['max_length'].format(max_length=max_length) - self.validators.append(MaxLengthValidator(max_length, message=message)) - if min_length is not None: - message = self.error_messages['min_length'].format(min_length=min_length) - self.validators.append(MinLengthValidator(min_length, message=message)) + if self.max_length is not None: + message = self.error_messages['max_length'].format(max_length=self.max_length) + self.validators.append(MaxLengthValidator(self.max_length, message=message)) + if self.min_length is not None: + message = self.error_messages['min_length'].format(min_length=self.min_length) + self.validators.append(MinLengthValidator(self.min_length, message=message)) def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, @@ -658,15 +658,15 @@ class IntegerField(Field): MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. def __init__(self, **kwargs): - max_value = kwargs.pop('max_value', None) - min_value = kwargs.pop('min_value', None) + self.max_value = kwargs.pop('max_value', None) + self.min_value = kwargs.pop('min_value', None) super(IntegerField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) + self.validators.append(MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: @@ -692,15 +692,15 @@ class FloatField(Field): MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. def __init__(self, **kwargs): - max_value = kwargs.pop('max_value', None) - min_value = kwargs.pop('min_value', None) + self.max_value = kwargs.pop('max_value', None) + self.min_value = kwargs.pop('min_value', None) super(FloatField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) + self.validators.append(MaxValueValidator(self.max_value, message=message)) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH: @@ -733,12 +733,14 @@ class DecimalField(Field): self.max_digits = max_digits self.decimal_places = decimal_places self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string + self.max_value = kwargs.pop('max_value', None) + self.min_value = kwargs.pop('min_value', None) super(DecimalField, self).__init__(**kwargs) - if max_value is not None: - message = self.error_messages['max_value'].format(max_value=max_value) + if self.max_value is not None: + message = self.error_messages['max_value'].format(max_value=self.max_value) self.validators.append(MaxValueValidator(max_value, message=message)) - if min_value is not None: - message = self.error_messages['min_value'].format(min_value=min_value) + if self.min_value is not None: + message = self.error_messages['min_value'].format(min_value=self.min_value) self.validators.append(MinValueValidator(min_value, message=message)) def to_internal_value(self, data): diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 3b058fabb..bf3611aa3 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -115,7 +115,13 @@ class SimpleMetadata(BaseMetadata): field_info['type'] = self.label_lookup[field] field_info['required'] = getattr(field, 'required', False) - for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']: + attrs = [ + 'read_only', 'label', 'help_text', + 'min_length', 'max_length', + 'min_value', 'max_value' + ] + + for attr in attrs: value = getattr(field, attr, None) if value is not None and value != '': field_info[attr] = force_text(value, strings_only=True) From bb8690cfb3208440c35a5c35eb65562f7e1729cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 20 Feb 2015 11:43:12 -0400 Subject: [PATCH 210/301] Disable select field if no choices available --- .../rest_framework/horizontal/select_multiple.html | 9 +++++++-- .../templates/rest_framework/inline/select_multiple.html | 9 +++++++-- .../rest_framework/vertical/select_multiple.html | 9 +++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html index 01c251fb0..0735f2809 100644 --- a/rest_framework/templates/rest_framework/horizontal/select_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html @@ -1,11 +1,16 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +

    {% if field.label %} {% endif %}
    - {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %} {% if field.errors %} diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html index feddf7abd..5a8b2494b 100644 --- a/rest_framework/templates/rest_framework/inline/select_multiple.html +++ b/rest_framework/templates/rest_framework/inline/select_multiple.html @@ -1,10 +1,15 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +
    {% if field.label %} {% endif %} - {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %}
    diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html index 54839294a..81b25c2a3 100644 --- a/rest_framework/templates/rest_framework/vertical/select_multiple.html +++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html @@ -1,10 +1,15 @@ +{% load i18n %} +{% trans "No items to select." as no_items %} +
    {% if field.label %} {% endif %} - {% for key, text in field.choices.items %} - + + {% empty %} + {% endfor %} {% if field.errors %} From 9cb547b85f547ef3b48f45710aee43c7cdd8b547 Mon Sep 17 00:00:00 2001 From: Rense VanderHoek Date: Fri, 20 Feb 2015 17:34:49 +0100 Subject: [PATCH 211/301] Validator-fix, added min/max fields to test_metadata --- rest_framework/fields.py | 4 ++-- tests/test_metadata.py | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 561ec93c2..1474f1db4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -738,10 +738,10 @@ class DecimalField(Field): super(DecimalField, self).__init__(**kwargs) if self.max_value is not None: message = self.error_messages['max_value'].format(max_value=self.max_value) - self.validators.append(MaxValueValidator(max_value, message=message)) + self.validators.append(MaxValueValidator(self.max_value, message=message)) if self.min_value is not None: message = self.error_messages['min_value'].format(min_value=self.min_value) - self.validators.append(MinValueValidator(min_value, message=message)) + self.validators.append(MinValueValidator(self.min_value, message=message)) def to_internal_value(self, data): """ diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 5031c0f30..3a435f02f 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -54,8 +54,12 @@ class TestMetadata: """ class ExampleSerializer(serializers.Serializer): choice_field = serializers.ChoiceField(['red', 'green', 'blue']) - integer_field = serializers.IntegerField(max_value=10) - char_field = serializers.CharField(required=False) + integer_field = serializers.IntegerField( + min_value=1, max_value=1000 + ) + char_field = serializers.CharField( + required=False, min_length=3, max_length=40 + ) class ExampleView(views.APIView): """Example view.""" @@ -96,13 +100,18 @@ class TestMetadata: 'type': 'integer', 'required': True, 'read_only': False, - 'label': 'Integer field' + 'label': 'Integer field', + 'min_value': 1, + 'max_value': 1000, + }, 'char_field': { 'type': 'string', 'required': False, 'read_only': False, - 'label': 'Char field' + 'label': 'Char field', + 'min_length': 3, + 'max_length': 40 } } } From 7345830c88615839891f12fd4ed6abee99bb1468 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Fri, 20 Feb 2015 20:12:39 +0100 Subject: [PATCH 212/301] Check if sessions are enabled before calling logout. Closes #2545. --- rest_framework/test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 4f4b7c201..a83d082ab 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -209,7 +209,8 @@ class APIClient(APIRequestFactory, DjangoClient): self.handler._force_user = None self.handler._force_token = None - return super(APIClient, self).logout() + if self.session: + super(APIClient, self).logout() class APITransactionTestCase(testcases.TransactionTestCase): From f29b657798d3f2223275fb33ca95fab2209fc229 Mon Sep 17 00:00:00 2001 From: ludbek Date: Sat, 21 Feb 2015 07:52:56 +0545 Subject: [PATCH 213/301] updated outdated link at testing.md#APIClient --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index d9a1696dd..9dc3f2bf1 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -115,7 +115,7 @@ Extends [Django's existing `Client` class][client]. ## Making requests -The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: +The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: from rest_framework.test import APIClient @@ -269,6 +269,6 @@ For example, to add support for using `format='html'` in test requests, you migh } [cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper -[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client +[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client [requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory [configuration]: #configuration From 91416632a86f518a043ac1b82da3d1774701ba96 Mon Sep 17 00:00:00 2001 From: Rense VanderHoek Date: Sat, 21 Feb 2015 12:31:37 +0100 Subject: [PATCH 214/301] DecimalField fix max_value and min_value are not in kwargs --- rest_framework/fields.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1474f1db4..13ea6dde7 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -733,9 +733,12 @@ class DecimalField(Field): self.max_digits = max_digits self.decimal_places = decimal_places self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string - self.max_value = kwargs.pop('max_value', None) - self.min_value = kwargs.pop('min_value', None) + + self.max_value = max_value + self.min_value = min_value + super(DecimalField, self).__init__(**kwargs) + if self.max_value is not None: message = self.error_messages['max_value'].format(max_value=self.max_value) self.validators.append(MaxValueValidator(self.max_value, message=message)) From bdc64d4e7370575a70a167dc2ae5d159610ce184 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Wed, 25 Feb 2015 11:54:11 +0100 Subject: [PATCH 215/301] Fix removal of url_path on @detail_route and @list_route. Fix # #2583 SimpleRouter.get_routes was popping out the url_path kwarg from list_route and detail_route decorators. This was causing troubles when the route was re-used, for example if the viewset was inherited. --- rest_framework/routers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 6a4184e20..081654b8c 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -171,9 +171,9 @@ class SimpleRouter(BaseRouter): # Dynamic detail routes (@detail_route decorator) for httpmethods, methodname in detail_routes: method_kwargs = getattr(viewset, methodname).kwargs - url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname ret.append(Route( url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), @@ -184,9 +184,9 @@ class SimpleRouter(BaseRouter): # Dynamic list routes (@list_route decorator) for httpmethods, methodname in list_routes: method_kwargs = getattr(viewset, methodname).kwargs - url_path = method_kwargs.pop("url_path", None) or methodname initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname ret.append(Route( url=replace_methodname(route.url, url_path), mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), From 9cafdd1854ccd5215b7a188c5896fb498a59d725 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Tue, 24 Feb 2015 17:14:53 +0100 Subject: [PATCH 216/301] Add a test for #2583 fix --- tests/test_routers.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_routers.py b/tests/test_routers.py index 948c69bbf..08c58ec70 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -302,12 +302,16 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet): return Response({'method': 'link2'}) +class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet): + pass + + class TestDynamicListAndDetailRouter(TestCase): def setUp(self): self.router = SimpleRouter() - def test_list_and_detail_route_decorators(self): - routes = self.router.get_routes(DynamicListAndDetailViewSet) + def _test_list_and_detail_route_decorators(self, viewset): + routes = self.router.get_routes(viewset) decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))] MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path') @@ -336,3 +340,9 @@ class TestDynamicListAndDetailRouter(TestCase): else: method_map = 'get' self.assertEqual(route.mapping[method_map], method_name) + + def test_list_and_detail_route_decorators(self): + self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet) + + def test_inherited_list_and_detail_route_decorators(self): + self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet) From 940cf2e2e004f913d3cc260fa2b490d33a163b51 Mon Sep 17 00:00:00 2001 From: Yannick PEROUX Date: Wed, 25 Feb 2015 13:29:07 +0100 Subject: [PATCH 217/301] Remove duplicated code in routers.SimpleRouter --- rest_framework/routers.py | 40 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 081654b8c..b1e39ff7d 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -165,34 +165,30 @@ class SimpleRouter(BaseRouter): else: list_routes.append((httpmethods, methodname)) + def _get_dynamic_routes(route, dynamic_routes): + ret = [] + for httpmethods, methodname in dynamic_routes: + method_kwargs = getattr(viewset, methodname).kwargs + initkwargs = route.initkwargs.copy() + initkwargs.update(method_kwargs) + url_path = initkwargs.pop("url_path", None) or methodname + ret.append(Route( + url=replace_methodname(route.url, url_path), + mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), + name=replace_methodname(route.name, url_path), + initkwargs=initkwargs, + )) + + return ret + ret = [] for route in self.routes: if isinstance(route, DynamicDetailRoute): # Dynamic detail routes (@detail_route decorator) - for httpmethods, methodname in detail_routes: - method_kwargs = getattr(viewset, methodname).kwargs - initkwargs = route.initkwargs.copy() - initkwargs.update(method_kwargs) - url_path = initkwargs.pop("url_path", None) or methodname - ret.append(Route( - url=replace_methodname(route.url, url_path), - mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, url_path), - initkwargs=initkwargs, - )) + ret += _get_dynamic_routes(route, detail_routes) elif isinstance(route, DynamicListRoute): # Dynamic list routes (@list_route decorator) - for httpmethods, methodname in list_routes: - method_kwargs = getattr(viewset, methodname).kwargs - initkwargs = route.initkwargs.copy() - initkwargs.update(method_kwargs) - url_path = initkwargs.pop("url_path", None) or methodname - ret.append(Route( - url=replace_methodname(route.url, url_path), - mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), - name=replace_methodname(route.name, url_path), - initkwargs=initkwargs, - )) + ret += _get_dynamic_routes(route, list_routes) else: # Standard route ret.append(route) From 71619a02c58df8ee56533daa70e5cc5ece278cf7 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 17:58:54 +0100 Subject: [PATCH 218/301] Update third-party-resources.md --- docs/topics/third-party-resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index e26e3a2fa..3125601ba 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -188,6 +188,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [hawkrest][hawkrest] - Provides Hawk HTTP Authorization. * [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism. * [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. +* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. ### Permissions From e51dc1855c2e0b2c079d5e248e58afea5bc016f7 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 18:51:20 +0100 Subject: [PATCH 219/301] Update authentication.md --- docs/api-guide/authentication.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 4b8110bd6..fe1be7bf0 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -353,6 +353,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system. +## django-rest-auth + +[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management. + [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -392,3 +396,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [mohawk]: http://mohawk.readthedocs.org/en/latest/ [mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 [djoser]: https://github.com/sunscrapers/djoser +[django-rest-auth]: https://github.com/Tivix/django-rest-auth From b92d6df66a761de697557cf66168a30db167e043 Mon Sep 17 00:00:00 2001 From: Mateusz Sikora Date: Wed, 25 Feb 2015 18:53:33 +0100 Subject: [PATCH 220/301] Update third-party-resources.md --- docs/topics/third-party-resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 3125601ba..2f46e1fc4 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -325,3 +325,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020 [web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/ [api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/ +[django-rest-auth]: https://github.com/Tivix/django-rest-auth/ From 86c5fa240131fe20121db707b0324a32967987ab Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Wed, 25 Feb 2015 16:20:45 +0100 Subject: [PATCH 221/301] Force-evaluate querysets (see #2602) --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9475e119b..2eef6eeb5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,6 +13,7 @@ response content is handled by parsers and renderers. from __future__ import unicode_literals from django.db import models from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField +from django.db.models import query from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta @@ -562,7 +563,7 @@ class ListSerializer(BaseSerializer): """ # Dealing with nested relationships, data can be a Manager, # so, first get a queryset from the Manager if needed - iterable = data.all() if isinstance(data, models.Manager) else data + iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data return [ self.child.to_representation(item) for item in iterable ] From 03818ed004cbe77459663f92a21691cfb7d9f010 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 12:48:34 +0000 Subject: [PATCH 222/301] Pagination tweaks and docs --- docs/api-guide/pagination.md | 164 ++++++++++++++++++++++++++++++++--- rest_framework/pagination.py | 6 +- 2 files changed, 157 insertions(+), 13 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index bae579a6d..697ba38d5 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -32,14 +32,14 @@ You can also set the pagination class on an individual view by using the `pagina 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 = 10000 + page_size = 1000 + page_size_query_param = 'page_size' + max_page_size = 10000 class StandardResultsSetPagination(PageNumberPagination): - paginate_by = 100 - paginate_by_param = 'page_size' - max_paginate_by = 1000 + page_size = 100 + page_size_query_param = 'page_size' + max_page_size = 1000 You can then apply your new style to a view using the `.pagination_class` attribute: @@ -59,15 +59,141 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. ## PageNumberPagination -**TODO** +This pagination style accepts a single number page number in the request query parameters. + +**Request**: + + GET https://api.example.org/accounts/?page=4 + +**Response**: + + HTTP 200 OK + { + "count": 1023 + "next": "https://api.example.org/accounts/?page=5", + "previous": "https://api.example.org/accounts/?page=3", + "results": [ + … + ] + } + +#### Setup + +To enable the `PageNumberPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'DEFAULT_PAGE_SIZE': 100 + } + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `PageNumberPagination` on a per-view basis. + +#### Configuration + +The `PageNumberPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `PageNumberPagination` class, and then enable your custom pagination class as above. + +* `page_size` - A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `page_query_param` - A string value indicating the name of the query parameter to use for the pagination control. +* `page_size_query_param` - If set, this is a string value indicating the name of a query parameter that allows the client to set the page size on a per-request basis. Defaults to `None`, indicating that the client may not control the requested page size. +* `max_page_size` - If set, this is a numeric value indicating the maximum allowable requested page size. This attribute is only valid if `page_size_query_param` is also set. +* `last_page_strings` - A list or tuple of string values indicating values that may be used with the `page_query_param` to request the final page in the set. Defaults to `('last',)` +* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`. + +--- ## LimitOffsetPagination -**TODO** +This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an +"offset" query parameter. The limit indicates the maximum number of items to return, and is equivalent to the `page_size` in other styles. The offset indicates the starting position of the query in relation to the complete set of unpaginated items. + +**Request**: + + GET https://api.example.org/accounts/?limit=100&offset=400 + +**Response**: + + HTTP 200 OK + { + "count": 1023 + "next": "https://api.example.org/accounts/?limit=100&offset=500", + "previous": "https://api.example.org/accounts/?limit=100&offset=300", + "results": [ + … + ] + } + +#### Setup + +To enable the `PageNumberPagination` style globally, use the following configuration: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' + } + +Optionally, you may also set a `DEFAULT_PAGE_SIZE` key. If the `DEFAULT_PAGE_SIZE` parameter is also used then the `limit` query parameter will be optional, and may be omitted by the client. + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `LimitOffsetPagination` on a per-view basis. + +#### Configuration + +The `LimitOffsetPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `LimitOffsetPagination` class, and then enable your custom pagination class as above. + +* `default_limit` - A numeric value indicating the limit to use if one is not provided by the client in a query parameter. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `limit_query_param` - A string value indicating the name of the "limit" query parameter. Defaults to `'limit'`. +* `offset_query_param` - A string value indicating the name of the "offset" query parameter. Defaults to `'offset'`. +* `max_limit` - If set this is a numeric value indicating the maximum allowable limit that may be requested by the client. Defaults to `None`. +* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`. + +--- ## CursorPagination -**TODO** +The cursor-based pagination presents an opaque "cursor" indicator that the client may use to page through the result set. This pagination style only presents forward and reverse controls, and does not allow the client to navigate to arbitrary positions. + +Cursor based pagination requires that there is a unique, unchanging ordering of items in the result set. This ordering might typically be a creation timestamp on the records, as this presents a consistent ordering to paginate against. + +Cursor based pagination is more complex than other schemes. It also requires that the result set presents a fixed ordering, and does not allow the client to arbitrarily index into the result set. However it does provide the following benefits: + +* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records. +* Supports usage with very large datasets. With extremely large datasets pagination using offset-based pagination styles may become inefficient or unusable. Cursor based pagination schemes instead have fixed-time properties, and do not slow down as the dataset size increases. + +#### Details and limitations + +This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering. + +It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed. + +**TODO**: Notes on `None`. + +The implementation also supports both forward and reverse pagination, which is often not supported in other implementations. + +For more technical details on the implementation we use for cursor pagination, the ["Building cursors for the Disqus API"][disqus-cursor-api] blog post gives a good overview of the basic approach. + +#### Setup + +To enable the `CursorPagination` style globally, use the following configuration, modifying the `DEFAULT_PAGE_SIZE` as desired: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', + 'DEFAULT_PAGE_SIZE': 100 + } + +On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `CursorPagination` on a per-view basis. + +#### Configuration + +The `CursorPagination` class includes a number of attributes that may be overridden to modify the pagination style. + +To set these attributes you should override the `CursorPagination` class, and then enable your custom pagination class as above. + +* `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. +* `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`. +* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. +* `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`. --- @@ -108,7 +234,7 @@ To have your custom pagination class be used by default, use the `DEFAULT_PAGINA REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination', - 'PAGINATE_BY': 10 + 'DEFAULT_PAGE_SIZE': 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: @@ -123,8 +249,25 @@ API responses for list endpoints will now include a `Link` header, instead of in # HTML pagination controls +By default using the pagination classes will cause HTML pagination controls to be displayed in the browsable API. There are two built-in display styles. The `PageNumberPagination` and `LimitOffsetPagination` classes display a list of page numbers with previous and next controls. The `CursorPagination` class displays a simpler style that only displays a previous and next control. + ## Customizing the controls +You can override the templates that render the HTML pagination controls. The two built-in styles are: + +* `rest_framework/pagination/numbers.html` +* `rest_framework/pagination/previous_and_next.html` + +Providing a template with either of these paths in a global template directory will override the default rendering for the relevant pagination classes. + +Alternatively you can disable HTML pagination controls completely by subclassing on of the existing classes, setting `template = None` as an attribute on the class. You'll then need to configure your `DEFAULT_PAGINATION_CLASS` settings key to use your custom class as the default pagination style. + +#### Low-level API + +The low-level API for determining if a pagination class should display the controls or not is exposed as a `display_page_controls` attribute on the pagination instance. Custom pagination classes should be set to `True` in the `paginate_queryset` method if they require the HTML pagination controls to be displayed. + +The `.to_html()` and `.get_html_context()` methods may also be overridden in a custom pagination class in order to further customize how the controls are rendered. + --- # Third party packages @@ -140,3 +283,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` [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 +[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ \ No newline at end of file diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 496500ba5..809858737 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -259,7 +259,7 @@ class PageNumberPagination(BasePagination): ) raise NotFound(msg) - if paginator.count > 1: + if paginator.count > 1 and self.template is not None: # The browsable API should display pagination controls. self.display_page_controls = True @@ -347,7 +347,7 @@ class LimitOffsetPagination(BasePagination): self.offset = self.get_offset(request) self.count = _get_count(queryset) self.request = request - if self.count > self.limit: + if self.count > self.limit and self.template is not None: self.display_page_controls = True return queryset[self.offset:self.offset + self.limit] @@ -518,7 +518,7 @@ class CursorPagination(BasePagination): # Display page controls in the browsable API if there is more # than one page. - if self.has_previous or self.has_next: + if (self.has_previous or self.has_next) and self.template is not None: self.display_page_controls = True return self.page From e72428214c33b889f61ac83c7f39030b4c66317d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 12:53:24 +0000 Subject: [PATCH 223/301] Formally upgrade suport to Django 1.8-beta --- README.md | 2 +- docs/index.md | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index eec809779..045cdbc46 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta) # Installation diff --git a/docs/index.md b/docs/index.md index 23781419f..91766a0b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ Some reasons you might want to use REST framework: REST framework requires the following: * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta) The following packages are optional: diff --git a/tox.ini b/tox.ini index b96b4939b..f626268c8 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps = django15: Django==1.5.6 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported - django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ + django18alpha: https://www.djangoproject.com/download/1.8b1/tarball/ -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 8f988466a594f9c0b6a7e6a2ed76c0b27a7f1895 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:20:26 +0000 Subject: [PATCH 224/301] Docs on exception handler context. Closes #2604. --- docs/api-guide/exceptions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 56811ec33..3e4b3e8be 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -47,7 +47,7 @@ Any example validation error might look like this: You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. -The function must take a single argument, which is the exception to be handled, and should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. +The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: @@ -72,6 +72,8 @@ In order to alter the style of the response, you could write the following custo return response +The context argument is not used by the default handler, but can be useful if the exception handler needs further information such as the view currently being handled, which can be accessed as `context['view']`. + The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example: REST_FRAMEWORK = { From b3956bc591e7bd2c0d1460cdbc2731a372df25a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:23:05 +0000 Subject: [PATCH 225/301] Upgrade testing env name to django18beta --- .travis.yml | 8 ++++---- tox.ini | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4f9297853..3eb89dc4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,10 +21,10 @@ env: - TOX_ENV=py26-django15 - TOX_ENV=py27-django14 - TOX_ENV=py26-django14 - - TOX_ENV=py34-django18alpha - - TOX_ENV=py33-django18alpha - - TOX_ENV=py32-django18alpha - - TOX_ENV=py27-django18alpha + - TOX_ENV=py34-django18beta + - TOX_ENV=py33-django18beta + - TOX_ENV=py32-django18beta + - TOX_ENV=py27-django18beta install: - pip install tox diff --git a/tox.ini b/tox.ini index f626268c8..c986250c5 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ envlist = py27-{flake8,docs}, {py26,py27}-django14, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18alpha} + {py27,py32,py33,py34}-django{17,18beta} [testenv] commands = ./runtests.py --fast @@ -14,7 +14,7 @@ deps = django15: Django==1.5.6 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported - django18alpha: https://www.djangoproject.com/download/1.8b1/tarball/ + django18beta: https://www.djangoproject.com/download/1.8b1/tarball/ -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 4b745eef3a452b79bac0fc2e7703aa0ade6836fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:25:14 +0000 Subject: [PATCH 226/301] Update test for more graceful 1.8 handling of malformed filename encodings --- tests/test_parsers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 8816065ab..a9f32a65e 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -101,9 +101,10 @@ class TestFileUploadParser(TestCase): self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') filename = parser.get_filename(self.stream, None, self.parser_context) - # Malformed. Either None or 'fallback.txt' will be acceptable. + + # Malformed. Either None, 'ÀĥƦ.txt' or 'fallback.txt' will be acceptable. # See also https://code.djangoproject.com/ticket/24209 - self.assertIn(filename, ('fallback.txt', None)) + self.assertIn(filename, ('fallback.txt', 'ÀĥƦ.txt', None)) def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition From 1b398a20decbf6e10173d280bc4fccd86a94b629 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Feb 2015 13:41:25 +0000 Subject: [PATCH 227/301] Who care what we do when it's totally malformed? Not me. --- tests/test_parsers.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index a9f32a65e..fe6aec196 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -99,12 +99,5 @@ class TestFileUploadParser(TestCase): filename = parser.get_filename(self.stream, None, self.parser_context) self.assertEqual(filename, 'ÀĥƦ.txt') - self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') - filename = parser.get_filename(self.stream, None, self.parser_context) - - # Malformed. Either None, 'ÀĥƦ.txt' or 'fallback.txt' will be acceptable. - # See also https://code.djangoproject.com/ticket/24209 - self.assertIn(filename, ('fallback.txt', 'ÀĥƦ.txt', None)) - def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition From 16ffe5e31f80058389139fe5dae5184cc22319a6 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 08:34:14 -0800 Subject: [PATCH 228/301] Add tests for callable attributes raising exceptions --- tests/test_fields.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index ab3418bd6..7f5f81029 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -93,6 +93,31 @@ class TestSource: "same as the field name. Remove the `source` keyword argument." ) + def test_callable_source(self): + class ExampleSerializer(serializers.Serializer): + example_field = serializers.CharField(source='example_callable') + + class ExampleInstance(object): + def example_callable(self): + return 'example callable value' + + serializer = ExampleSerializer(ExampleInstance()) + assert serializer.data['example_field'] == 'example callable value' + + def test_callable_source_raises(self): + class ExampleSerializer(serializers.Serializer): + example_field = serializers.CharField(source='example_callable', read_only=True) + + class ExampleInstance(object): + def example_callable(self): + raise AttributeError('method call failed') + + with pytest.raises(ValueError) as exc_info: + serializer = ExampleSerializer(ExampleInstance()) + serializer.data.items() + + assert 'method call failed' in str(exc_info.value) + class TestReadOnly: def setup(self): From bdb73d558891192c96368d5ca2266327302dba54 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 09:00:51 -0800 Subject: [PATCH 229/301] Avoid swallowing exceptions thrown in callable attributes --- rest_framework/fields.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a5348922a..01e7c78c8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -71,7 +71,11 @@ def get_attribute(instance, attrs): except ObjectDoesNotExist: return None if is_simple_callable(instance): - instance = instance() + try: + instance = instance() + except (AttributeError, KeyError) as exc: + raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc)) + return instance From e6b06c34c1ee526b65c92b9071c47be2ddc668c4 Mon Sep 17 00:00:00 2001 From: Evan Heidtmann Date: Thu, 26 Feb 2015 09:20:17 -0800 Subject: [PATCH 230/301] Add explanation for this exception mutation --- rest_framework/fields.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 01e7c78c8..f2791a13f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -74,6 +74,9 @@ def get_attribute(instance, attrs): try: instance = instance() except (AttributeError, KeyError) as exc: + # If we raised an Attribute or KeyError here it'd get treated + # as an omitted field in `Field.get_attribute()`. Instead we + # raise a ValueError to ensure the exception is not masked. raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc)) return instance From 32c885c2a0ddd296b17198cbcce27f539bf39456 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Fri, 27 Feb 2015 15:22:19 +0000 Subject: [PATCH 231/301] Ensure validators are new-style classes on python2 --- rest_framework/validators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index ab3616149..6ae80b897 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.utils.representation import smart_repr -class UniqueValidator: +class UniqueValidator(object): """ Validator that corresponds to `unique=True` on a model field. @@ -67,7 +67,7 @@ class UniqueValidator: )) -class UniqueTogetherValidator: +class UniqueTogetherValidator(object): """ Validator that corresponds to `unique_together = (...)` on a model class. @@ -155,7 +155,7 @@ class UniqueTogetherValidator: )) -class BaseUniqueForValidator: +class BaseUniqueForValidator(object): message = None missing_message = _('This field is required.') From 9c359181d7e897e796bd38f0b16e6ddd5ae70d86 Mon Sep 17 00:00:00 2001 From: aRkadeFR Date: Fri, 27 Feb 2015 17:38:28 +0100 Subject: [PATCH 232/301] update for `that the` instead of `that` --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 1b96b325e..ed8bbd1d0 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -14,7 +14,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. ## Creating test requests -The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. +The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that the standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. from rest_framework.test import APIRequestFactory From 9098856d46f2bf1a5f191b48b5e7b7e07add4dc7 Mon Sep 17 00:00:00 2001 From: Janusz Harkot Date: Fri, 27 Feb 2015 19:46:36 +0100 Subject: [PATCH 233/301] fix DictKey initial value --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c0f93816a..13301f31b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1191,7 +1191,7 @@ class ListField(Field): class DictField(Field): child = _UnvalidatedField() - initial = [] + initial = {} default_error_messages = { 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".') } From 78e8b1b0108bb8338aa578ea2f4a0237d4edd1d4 Mon Sep 17 00:00:00 2001 From: Kevin Wood Date: Fri, 27 Feb 2015 22:14:15 -0800 Subject: [PATCH 234/301] Updated CreateOnlyDefault to call set_context on its default (if callable) --- rest_framework/fields.py | 2 ++ tests/test_fields.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 13301f31b..c327f11bc 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -114,6 +114,8 @@ class CreateOnlyDefault: def set_context(self, serializer_field): self.is_update = serializer_field.parent.instance is not None + if callable(self.default) and hasattr(self.default, 'set_context'): + self.default.set_context(serializer_field) def __call__(self): if self.is_update: diff --git a/tests/test_fields.py b/tests/test_fields.py index 7f5f81029..2ffffd55a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -317,6 +317,22 @@ class TestCreateOnlyDefault: 'text': 'example', } + def test_create_only_default_callable_sets_context(self): + """ CreateOnlyDefault instances with a callable default should set_context on the callable if possible """ + class TestCallableDefault: + def set_context(self, serializer_field): + self.field = serializer_field + + def __call__(self): + return "success" if hasattr(self, 'field') else "failure" + + class TestSerializer(serializers.Serializer): + context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault())) + + serializer = TestSerializer(data={}) + assert serializer.is_valid() + assert serializer.validated_data['context_set'] == 'success' + # Tests for field input and output values. # ---------------------------------------- From b582d52afbad81c56edad8ea3b6d2ac2d352b87e Mon Sep 17 00:00:00 2001 From: Kevin Wood Date: Sat, 28 Feb 2015 13:06:47 -0800 Subject: [PATCH 235/301] Fix docstring formatting --- tests/test_fields.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 2ffffd55a..1aa528da6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -318,7 +318,10 @@ class TestCreateOnlyDefault: } def test_create_only_default_callable_sets_context(self): - """ CreateOnlyDefault instances with a callable default should set_context on the callable if possible """ + """ + CreateOnlyDefault instances with a callable default should set_context + on the callable if possible + """ class TestCallableDefault: def set_context(self, serializer_field): self.field = serializer_field From 391b0ae21b29212764c4f3d079187d07228bb743 Mon Sep 17 00:00:00 2001 From: Tymur Maryokhin Date: Tue, 3 Mar 2015 17:02:12 +0100 Subject: [PATCH 236/301] Call default.set_context() only on create. Refs #2619. --- rest_framework/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c327f11bc..a80862e8c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -103,7 +103,7 @@ def set_value(dictionary, keys, value): dictionary[keys[-1]] = value -class CreateOnlyDefault: +class CreateOnlyDefault(object): """ This class may be used to provide default values that are only used for create operations, but that do not return any value for update @@ -114,7 +114,7 @@ class CreateOnlyDefault: def set_context(self, serializer_field): self.is_update = serializer_field.parent.instance is not None - if callable(self.default) and hasattr(self.default, 'set_context'): + if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update: self.default.set_context(serializer_field) def __call__(self): @@ -130,7 +130,7 @@ class CreateOnlyDefault: ) -class CurrentUserDefault: +class CurrentUserDefault(object): def set_context(self, serializer_field): self.user = serializer_field.context['request'].user From fdd811ec53b3bdc46a2c934422066e1aa9f9dd05 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Wed, 4 Mar 2015 08:22:46 +0300 Subject: [PATCH 237/301] Allow blank/null on radio.html choices --- .../rest_framework/horizontal/radio.html | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/radio.html b/rest_framework/templates/rest_framework/horizontal/radio.html index 52238bb1a..efca2883e 100644 --- a/rest_framework/templates/rest_framework/horizontal/radio.html +++ b/rest_framework/templates/rest_framework/horizontal/radio.html @@ -1,20 +1,36 @@ +{% load i18n %} +
    {% if field.label %} {% endif %}
    {% if style.inline %} + {% if field.allow_null or field.allow_blank %} + + {% endif %} {% for key, text in field.choices.items %} {% endfor %} {% else %} + {% if field.allow_null or field.allow_blank %} +
    + +
    + {% endif %} {% for key, text in field.choices.items %}
    From 18cc0230bff436da2f26b2b25034cece32c9f5d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Mar 2015 15:51:00 +0000 Subject: [PATCH 238/301] Clean up pagination attributes --- docs/api-guide/generic-views.md | 7 +- .../5-relationships-and-hyperlinked-apis.md | 2 +- docs/tutorial/quickstart.md | 2 +- rest_framework/pagination.py | 73 ++++++++++++++----- rest_framework/settings.py | 11 ++- tests/test_pagination.py | 8 +- 6 files changed, 71 insertions(+), 32 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 61c8e8d88..39e09aaa5 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -84,10 +84,9 @@ The following attributes control the basic view behavior. The following attributes are used to control pagination when used with list views. -* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`. -* `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`. -* `pagination_serializer_class` - The pagination serializer class to use when determining the style of paginated responses. Defaults to the same value as the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting. -* `page_kwarg` - The name of a URL kwarg or URL query parameter which can be used by the client to control which page is requested. Defaults to `'page'`. +* `pagination_class` - The pagination class that should be used when paginating list results. Defaults to the same value as the `DEFAULT_PAGINATION_CLASS` setting, which is `'rest_framework.pagination.PageNumberPagination'`. + +Note that usage of the `paginate_by`, `paginate_by_param` and `page_kwarg` attributes are now pending deprecation. The `pagination_serializer_class` attribute and `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting have been removed completely. Pagination settings should instead be controlled by overriding a pagination class and setting any configuration attributes there. See the pagination documentation for more details. **Filtering**: diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 740a4ce21..91cdd6f10 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -141,7 +141,7 @@ The list views for users and code snippets could end up returning quite a lot of We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting: REST_FRAMEWORK = { - 'PAGINATE_BY': 10 + 'PAGE_SIZE': 10 } Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well separated from your other project settings. diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index a4474c34e..fe0ecbc7e 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -123,7 +123,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), - 'PAGINATE_BY': 10 + 'PAGE_SIZE': 10 } Okay, we're done. diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 809858737..6a2f5b271 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -18,6 +18,7 @@ from rest_framework.settings import api_settings from rest_framework.utils.urls import ( replace_query_param, remove_query_param ) +import warnings def _positive_int(integer_string, strict=False, cutoff=None): @@ -203,18 +204,18 @@ class PageNumberPagination(BasePagination): """ # The default page size. # Defaults to `None`, meaning pagination is disabled. - paginate_by = api_settings.PAGINATE_BY + page_size = api_settings.PAGE_SIZE # 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 + page_size_query_param = None # 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 + # Only relevant if 'page_size_query_param' has also been set. + max_page_size = None last_page_strings = ('last',) @@ -228,12 +229,48 @@ class PageNumberPagination(BasePagination): attributes were set there. The attributes should now be set on the pagination class, but the old style is still pending deprecation. """ - for attr in ( - 'paginate_by', 'page_query_param', - 'paginate_by_param', 'max_paginate_by' + assert not ( + getattr(view, 'pagination_serializer_class', None) or + getattr(api_settings, 'DEFAULT_PAGINATION_SERIALIZER_CLASS', None) + ), ( + "The pagination_serializer_class attribute and " + "DEFAULT_PAGINATION_SERIALIZER_CLASS setting have been removed as " + "part of the 3.1 pagination API improvement. See the pagination " + "documentation for details on the new API." + ) + + for (settings_key, attr_name) in ( + ('PAGINATE_BY', 'page_size'), + ('PAGINATE_BY_PARAM', 'page_size_query_param'), + ('MAX_PAGINATE_BY', 'max_page_size') ): - if hasattr(view, attr): - setattr(self, attr, getattr(view, attr)) + value = getattr(api_settings, settings_key, None) + if value is not None: + setattr(self, attr_name, value) + warnings.warn( + "The `%s` settings key is pending deprecation. " + "Use the `%s` attribute on the pagination class instead." % ( + settings_key, attr_name + ), + PendingDeprecationWarning, + ) + + for (view_attr, attr_name) in ( + ('paginate_by', 'page_size'), + ('page_query_param', 'page_query_param'), + ('paginate_by_param', 'page_size_query_param'), + ('max_paginate_by', 'max_page_size') + ): + value = getattr(view, view_attr, None) + if value is not None: + setattr(self, attr_name, value) + warnings.warn( + "The `%s` view attribute is pending deprecation. " + "Use the `%s` attribute on the pagination class instead." % ( + view_attr, attr_name + ), + PendingDeprecationWarning, + ) def paginate_queryset(self, queryset, request, view=None): """ @@ -264,7 +301,7 @@ class PageNumberPagination(BasePagination): self.display_page_controls = True self.request = request - return self.page + return list(self.page) def get_paginated_response(self, data): return Response(OrderedDict([ @@ -275,17 +312,17 @@ class PageNumberPagination(BasePagination): ])) def get_page_size(self, request): - if self.paginate_by_param: + if self.page_size_query_param: try: return _positive_int( - request.query_params[self.paginate_by_param], + request.query_params[self.page_size_query_param], strict=True, - cutoff=self.max_paginate_by + cutoff=self.max_page_size ) except (KeyError, ValueError): pass - return self.paginate_by + return self.page_size def get_next_link(self): if not self.page.has_next(): @@ -336,7 +373,7 @@ class LimitOffsetPagination(BasePagination): http://api.example.org/accounts/?limit=100 http://api.example.org/accounts/?offset=400&limit=100 """ - default_limit = api_settings.PAGINATE_BY + default_limit = api_settings.PAGE_SIZE limit_query_param = 'limit' offset_query_param = 'offset' max_limit = None @@ -349,7 +386,7 @@ class LimitOffsetPagination(BasePagination): self.request = request if self.count > self.limit and self.template is not None: self.display_page_controls = True - return queryset[self.offset:self.offset + self.limit] + return list(queryset[self.offset:self.offset + self.limit]) def get_paginated_response(self, data): return Response(OrderedDict([ @@ -440,7 +477,7 @@ class CursorPagination(BasePagination): # Consider a max offset cap. # Tidy up the `get_ordering` API (eg remove queryset from it) cursor_query_param = 'cursor' - page_size = api_settings.PAGINATE_BY + page_size = api_settings.PAGE_SIZE invalid_cursor_message = _('Invalid cursor') ordering = None template = 'rest_framework/pagination/previous_and_next.html' @@ -484,7 +521,7 @@ class CursorPagination(BasePagination): # We also always fetch an extra item in order to determine if there is a # page following on from this one. results = list(queryset[offset:offset + self.page_size + 1]) - self.page = results[:self.page_size] + self.page = list(results[:self.page_size]) # Determine the position of the final item following the page. if len(results) > len(self.page): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 394b12622..a3e9f5902 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -61,9 +61,7 @@ DEFAULTS = { 'NUM_PROXIES': None, # Pagination - 'PAGINATE_BY': None, - 'PAGINATE_BY_PARAM': None, - 'MAX_PAGINATE_BY': None, + 'PAGE_SIZE': None, # Filtering 'SEARCH_PARAM': 'search', @@ -117,7 +115,12 @@ DEFAULTS = { 'UNICODE_JSON': True, 'COMPACT_JSON': True, 'COERCE_DECIMAL_TO_STRING': True, - 'UPLOADED_FILES_USE_URL': True + 'UPLOADED_FILES_USE_URL': True, + + # Pending deprecation: + 'PAGINATE_BY': None, + 'PAGINATE_BY_PARAM': None, + 'MAX_PAGINATE_BY': None } diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 13bfb6272..6b39a6f22 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -24,9 +24,9 @@ class TestPaginationIntegration: return [item for item in queryset if item % 2 == 0] class BasicPagination(pagination.PageNumberPagination): - paginate_by = 5 - paginate_by_param = 'page_size' - max_paginate_by = 20 + page_size = 5 + page_size_query_param = 'page_size' + max_page_size = 20 self.view = generics.ListAPIView.as_view( serializer_class=PassThroughSerializer, @@ -185,7 +185,7 @@ class TestPageNumberPagination: def setup(self): class ExamplePagination(pagination.PageNumberPagination): - paginate_by = 5 + page_size = 5 self.pagination = ExamplePagination() self.queryset = range(1, 101) From efb42ff7d048d165b151e3b75553ef720dc49cd3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Mar 2015 16:17:30 +0000 Subject: [PATCH 239/301] Update docs --- docs/api-guide/pagination.md | 26 +++++++++++++++++++++++++- docs/topics/3.1-announcement.md | 9 +++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 697ba38d5..13bd57aef 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -208,6 +208,30 @@ Note that the `paginate_queryset` method may set state on the pagination instanc ## Example +Suppose we want to replace the default pagination output style with a modified format that includes the next and previous links under in a nested 'links' key. We could specify a custom pagination class like so: + + class CustomPagination(pagination.PageNumberPagination): + def get_paginated_response(self, data): + return Response({ + 'links': { + 'next': self.get_next_link(), + 'previous': self.get_previous_link() + }, + 'count': self.page.paginator.count, + 'results': data + }) + +We'd then need to setup the custom class in our configuration: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.CustomPagination', + 'PAGE_SIZE': 100 + } + +Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional. + +## Header based pagination + 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(pagination.PageNumberPagination): @@ -234,7 +258,7 @@ To have your custom pagination class be used by default, use the `DEFAULT_PAGINA REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination', - 'DEFAULT_PAGE_SIZE': 10 + 'PAGE_SIZE': 100 } 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: diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index f500101c5..ecbc9a380 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -17,6 +17,15 @@ Some highlights include: The pagination API has been improved, making it both easier to use, and more powerful. +A guide to the headline features follows. For full details, see [the pagination documentation][pagination]. + +Note that as a result of this work a number of settings keys and generic view attributes are now moved to pending deprecation. Controlling pagination styles is now largely handled by overriding a pagination class and modifying its configuration attributes. + +* The `PAGINATE_BY` settings key will continue to work but is now pending deprecation. The more obviously named `PAGE_SIZE` settings key should now be used instead. +* The `PAGINATE_BY_PARAM`, `MAX_PAGINATE_BY` settings keys will continue to work but are now pending deprecation, in favor of setting configuration attributes on the configured pagination class. +* The `paginate_by`, `page_query_param`, `paginate_by_param` and `max_paginate_by` generic view attributes will continue to work but are now pending deprecation, in favor of setting configuration attributes on the configured pagination class. +* The `pagination_serializer_class` view attribute and `DEFAULT_PAGINATION_SERIALIZER_CLASS` settings key **are no longer valid**. The pagination API does not use serializers to determine the output format, and you'll need to instead override the `get_paginated_response` method on a pagination class in order to specify how the output format is controlled. + #### New pagination schemes. Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default. From ce31e369734bd6db1a5a1a94bb1679e6bbbf34b3 Mon Sep 17 00:00:00 2001 From: Egor Yurtaev Date: Thu, 5 Mar 2015 18:34:42 +0600 Subject: [PATCH 240/301] Remove `MergeDict` The class MergeDict is deprecated and will be removed in Django 1.9 --- rest_framework/request.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index fd4f6a3e2..e4b5bc263 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -14,7 +14,6 @@ from django.http import QueryDict from django.http.multipartparser import parse_header from django.utils import six from django.utils.datastructures import MultiValueDict -from django.utils.datastructures import MergeDict as DjangoMergeDict from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.settings import api_settings @@ -61,15 +60,6 @@ class override_method(object): self.view.action = self.action -class MergeDict(DjangoMergeDict, dict): - """ - Using this as a workaround until the parsers API is properly - addressed in 3.1. - """ - def __init__(self, *dicts): - self.dicts = dicts - - class Empty(object): """ Placeholder for unset attributes. @@ -328,7 +318,8 @@ class Request(object): if not _hasattr(self, '_data'): self._data, self._files = self._parse() if self._files: - self._full_data = MergeDict(self._data, self._files) + self._full_data = self._data.copy() + self._full_data.update(self._files) else: self._full_data = self._data @@ -392,7 +383,8 @@ class Request(object): # At this point we're committed to parsing the request as form data. self._data = self._request.POST self._files = self._request.FILES - self._full_data = MergeDict(self._data, self._files) + self._full_data = self._data.copy() + self._full_data.update(self._files) # Method overloading - change the method and remove the param from the content. if ( From 58dfde7fcd9c29530d0613161dda0cf30c56a0a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Mar 2015 10:22:32 +0000 Subject: [PATCH 241/301] Tweaks for cursor pagination and docs --- docs/api-guide/pagination.md | 12 +++++++++--- docs/topics/3.1-announcement.md | 1 + docs/topics/release-notes.md | 5 +++++ rest_framework/pagination.py | 28 ++++++++++++++++++---------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 13bd57aef..14c0b7f27 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -51,7 +51,8 @@ You can then apply your new style to a view using the `.pagination_class` attrib Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example: REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' } + 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' + } --- @@ -163,6 +164,10 @@ Cursor based pagination is more complex than other schemes. It also requires tha #### Details and limitations +Cursor based pagination requires a specified ordering to be applied to the queryset. This will default to `'-created'`, which requires the model being paged against to have a `'created'` field. + + + This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering. It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed. @@ -192,7 +197,7 @@ To set these attributes you should override the `CursorPagination` class, and th * `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. * `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`. -* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. +* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = '-created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. * `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`. --- @@ -236,7 +241,8 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu class LinkHeaderPagination(pagination.PageNumberPagination): def get_paginated_response(self, data): - next_url = self.get_next_link() previous_url = self.get_previous_link() + next_url = self.get_next_link() + previous_url = self.get_previous_link() if next_url is not None and previous_url is not None: link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">' diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index ecbc9a380..6606d8431 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -203,3 +203,4 @@ The next focus will be on HTML renderings of API output and will include: This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release. [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling +[pagination]: ../api-guide/pagination.md diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 51eb45c37..35592febe 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`: ## 3.0.x series +### 3.1.0 + +**Date**: [5th March 2015][3.1.0-milestone]. + +For full details see the [3.1 release announcement](3.1-announcement.md). ### 3.0.5 diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6a2f5b271..f41a9ae1a 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -131,12 +131,19 @@ def _decode_cursor(encoded): """ Given a string representing an encoded cursor, return a `Cursor` instance. """ + + # The offset in the cursor is used in situations where we have a + # nearly-unique index. (Eg millisecond precision creation timestamps) + # We guard against malicious users attempting to cause expensive database + # queries, by having a hard cap on the maximum possible size of the offset. + OFFSET_CUTOFF = 1000 + try: querystring = b64decode(encoded.encode('ascii')).decode('ascii') tokens = urlparse.parse_qs(querystring, keep_blank_values=True) offset = tokens.get('o', ['0'])[0] - offset = _positive_int(offset) + offset = _positive_int(offset, cutoff=OFFSET_CUTOFF) reverse = tokens.get('r', ['0'])[0] reverse = bool(int(reverse)) @@ -472,14 +479,15 @@ class LimitOffsetPagination(BasePagination): class CursorPagination(BasePagination): - # Determine how/if True, False and None positions work - do the string - # encodings work with Django queryset filters? - # Consider a max offset cap. - # Tidy up the `get_ordering` API (eg remove queryset from it) + """ + The cursor pagination implementation is neccessarily complex. + For an overview of the position/offset style we use, see this post: + http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ + """ cursor_query_param = 'cursor' page_size = api_settings.PAGE_SIZE invalid_cursor_message = _('Invalid cursor') - ordering = None + ordering = '-created' template = 'rest_framework/pagination/previous_and_next.html' def paginate_queryset(self, queryset, request, view=None): @@ -680,12 +688,12 @@ class CursorPagination(BasePagination): ) ) else: - # The default case is to check for an `ordering` attribute, - # first on the view instance, and then on this pagination instance. - ordering = getattr(view, 'ordering', getattr(self, 'ordering', None)) + # The default case is to check for an `ordering` attribute + # on this pagination instance. + ordering = self.ordering assert ordering is not None, ( 'Using cursor pagination, but no ordering attribute was declared ' - 'on the view or on the pagination class.' + 'on the pagination class.' ) assert isinstance(ordering, (six.string_types, list, tuple)), ( From c511342047f9eea01335fec0ac9ff7f4c823b696 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Mar 2015 11:32:03 +0000 Subject: [PATCH 242/301] More docs on cursor pagination --- docs/api-guide/pagination.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 14c0b7f27..bc65267fb 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -159,22 +159,23 @@ Cursor based pagination requires that there is a unique, unchanging ordering of Cursor based pagination is more complex than other schemes. It also requires that the result set presents a fixed ordering, and does not allow the client to arbitrarily index into the result set. However it does provide the following benefits: -* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records. +* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records, even when new items are being inserted by other clients during the pagination process. * Supports usage with very large datasets. With extremely large datasets pagination using offset-based pagination styles may become inefficient or unusable. Cursor based pagination schemes instead have fixed-time properties, and do not slow down as the dataset size increases. #### Details and limitations -Cursor based pagination requires a specified ordering to be applied to the queryset. This will default to `'-created'`, which requires the model being paged against to have a `'created'` field. +Proper use of cursor based pagination a little attention to detail. You'll need to think about what ordering you want the scheme to be applied against. The default is to order by `"-created"`. This assumes that **there must be a 'created' timestamp field** on the model instances, and will present a "timeline" style paginated view, with the most recently added items first. +You can modify the ordering by overriding the `'ordering'` attribute on the pagination class, or by using the `OrderingFilter` filter class together with `CursorPagination`. When used with `OrderingFilter` you should strongly consider restricting the fields that the user may order by. +Proper usage of cursor pagination should have an ordering field that satisfies the following: -This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering. +* Should be an unchanging value, such as a timestamp, slug, or other field that is only set once, on creation. +* Should be unique, or nearly unique. Millisecond precision timestamps are a good example. This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering. +* Should be a non-nullable value that can be coerced to a string. +* The field should have a database index. -It should be noted that using non-unique values the ordering does introduce the possibility of paging artifacts, where pagination consistency is no longer 100% guaranteed. - -**TODO**: Notes on `None`. - -The implementation also supports both forward and reverse pagination, which is often not supported in other implementations. +Using an ordering field that does not satisfy these constraints will generally still work, but you'll be loosing some of the benefits of cursor pagination. For more technical details on the implementation we use for cursor pagination, the ["Building cursors for the Disqus API"][disqus-cursor-api] blog post gives a good overview of the basic approach. @@ -197,7 +198,7 @@ To set these attributes you should override the `CursorPagination` class, and th * `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key. * `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`. -* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = '-created'`. Any filters on the view which define a `get_ordering` will override this attribute. Defaults to `None`. +* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'slug'`. Defaults to `-created`. This value may also be overridden by using `OrderingFilter` on the view. * `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`. --- From 7efb2fd9ed03a23b0bcf8d9fa20034bf9ef884f8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Mar 2015 12:26:09 +0000 Subject: [PATCH 243/301] Better docs linking --- docs/topics/3.1-announcement.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 6606d8431..9cad88e07 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -52,7 +52,7 @@ For more information, see the [custom pagination styles](../api-guide/pagination ## Versioning -We've made it easier to build versioned APIs. Built-in schemes for versioning include both URL based and Accept header based variations. +We've made it [easier to build versioned APIs][versioning]. Built-in schemes for versioning include both URL based and Accept header based variations. When using a URL based scheme, hyperlinked serializers will resolve relationships to the same API version as used on the incoming request. @@ -80,7 +80,7 @@ The output representation would match the version used on the incoming request. ## Internationalization -REST framework now includes a built-in set of translations, and supports internationalized error responses. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header. +REST framework now includes a built-in set of translations, and [supports internationalized error responses][internationalization]. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header. You can change the default language by using the standard Django `LANGUAGE_CODE` setting: @@ -204,3 +204,5 @@ This will either be made as a single 3.2 release, or split across two separate r [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling [pagination]: ../api-guide/pagination.md +[versioning]: ../api-guide/versioning.md +[internationalization]: internationalization.md From d2181cc74ccfc59edc0477017d95e8edcbecf8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Fri, 6 Mar 2015 09:41:34 -0400 Subject: [PATCH 244/301] Fix customizing field mappings link --- docs/topics/3.1-announcement.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 9cad88e07..6eb3681ff 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -145,7 +145,7 @@ If you're building a new 1.8 project, then you should probably consider using `U The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships. -For more information, see the documentation on [customizing field mappings](../api-guide/serializers/#customizing-field-mappings) for ModelSerializer classes. +For more information, see the documentation on [customizing field mappings][customizing-field-mappings] for ModelSerializer classes. --- @@ -206,3 +206,4 @@ This will either be made as a single 3.2 release, or split across two separate r [pagination]: ../api-guide/pagination.md [versioning]: ../api-guide/versioning.md [internationalization]: internationalization.md +[customizing-field-mappings]: ../api-guide/serializers.md/#customizing-field-mappings From fb58ef043cc39d900bb8389855f07087cb0d7920 Mon Sep 17 00:00:00 2001 From: Matt d'Entremont Date: Wed, 4 Mar 2015 17:36:03 -0400 Subject: [PATCH 245/301] Add support for serializing models with m2m related fields - In both ManyRelatedField, provide an empty return when trying to access a relation field if the instance in question has no PK (so likely hasn't been inserted yet) - Add relevant tests - Without these changes, exceptions would be raised when trying to serialize the uncreated models as it is impossible to query relations without a PK - Add test to ensure RelatedField does not regress as currently supports being serialized with and unsaved model --- rest_framework/relations.py | 4 ++++ tests/test_relations_pk.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 0b7c9d864..3a966c5bf 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -360,6 +360,10 @@ class ManyRelatedField(Field): ] def get_attribute(self, instance): + # Can't have any relationships if not created + if not instance.pk: + return [] + relationship = get_attribute(instance, self.source_attrs) return relationship.all() if (hasattr(relationship, 'all')) else relationship diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index f872a8dc5..ca43272b0 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -143,6 +143,16 @@ class PKManyToManyTests(TestCase): ] self.assertEqual(serializer.data, expected) + def test_many_to_many_unsaved(self): + source = ManyToManySource(name='source-unsaved') + + serializer = ManyToManySourceSerializer(source) + + expected = {'id': None, 'name': 'source-unsaved', 'targets': []} + # no query if source hasn't been created yet + with self.assertNumQueries(0): + self.assertEqual(serializer.data, expected) + def test_reverse_many_to_many_create(self): data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) @@ -296,6 +306,16 @@ class PKForeignKeyTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'target': ['This field may not be null.']}) + def test_foreign_key_with_unsaved(self): + source = ForeignKeySource(name='source-unsaved') + expected = {'id': None, 'name': 'source-unsaved', 'target': None} + + serializer = ForeignKeySourceSerializer(source) + + # no query if source hasn't been created yet + with self.assertNumQueries(0): + self.assertEqual(serializer.data, expected) + def test_foreign_key_with_empty(self): """ Regression test for #1072 From 7159b31023640b8821131e39a7f9eaadfacb2f07 Mon Sep 17 00:00:00 2001 From: Aider Ibragimov Date: Sat, 7 Mar 2015 07:17:22 +0300 Subject: [PATCH 246/301] update vertical and inline layouts for radio choices --- .../rest_framework/horizontal/radio.html | 5 +++-- .../templates/rest_framework/inline/radio.html | 11 +++++++++++ .../rest_framework/vertical/radio.html | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/radio.html b/rest_framework/templates/rest_framework/horizontal/radio.html index efca2883e..cabd09d2b 100644 --- a/rest_framework/templates/rest_framework/horizontal/radio.html +++ b/rest_framework/templates/rest_framework/horizontal/radio.html @@ -1,4 +1,5 @@ {% load i18n %} +{% trans "None" as none_choice %}
    {% if field.label %} @@ -9,7 +10,7 @@ {% if field.allow_null or field.allow_blank %} {% endif %} {% for key, text in field.choices.items %} @@ -23,7 +24,7 @@
    {% endif %} diff --git a/rest_framework/templates/rest_framework/inline/radio.html b/rest_framework/templates/rest_framework/inline/radio.html index 1915f4f84..b65016715 100644 --- a/rest_framework/templates/rest_framework/inline/radio.html +++ b/rest_framework/templates/rest_framework/inline/radio.html @@ -1,7 +1,18 @@ +{% load i18n %} +{% trans "None" as none_choice %} +
    {% if field.label %} {% endif %} + {% if field.allow_null or field.allow_blank %} +
    + +
    + {% endif %} {% for key, text in field.choices.items %}