From d972df7c9c1867b4a0a57307f423a488c4d4f4b1 Mon Sep 17 00:00:00 2001
From: tanwanirahul
Date: Mon, 3 Nov 2014 14:43:53 +0100
Subject: [PATCH 001/192] Ability to override default method names by
customizing it
---
rest_framework/routers.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 169e6e8bc..d1c9fa1b9 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter):
if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in detail_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname
initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, custom_method_name),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, custom_method_name),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname
initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, custom_method_name),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, custom_method_name),
initkwargs=initkwargs,
))
else:
From ea8c40520165fc33343fceb15221b770701bdedf Mon Sep 17 00:00:00 2001
From: tanwanirahul
Date: Mon, 3 Nov 2014 14:44:47 +0100
Subject: [PATCH 002/192] Tests for validating custom_method_name router
attribute
---
tests/test_routers.py | 32 ++++++++++++++++++++++++++------
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index f6f5a977a..d426f8320 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -8,6 +8,7 @@ from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory
+from collections import namedtuple
factory = APIRequestFactory()
@@ -260,6 +261,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
+ @list_route(custom_method_name="list_custom-route")
+ def list_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route(custom_method_name="detail_custom-route")
+ def detail_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
class TestDynamicListAndDetailRouter(TestCase):
def setUp(self):
@@ -268,22 +277,33 @@ class TestDynamicListAndDetailRouter(TestCase):
def test_list_and_detail_route_decorators(self):
routes = self.router.get_routes(DynamicListAndDetailViewSet)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
+
+ MethodNamesMap = namedtuple('MethodNamesMap', 'method_name custom_method_name')
# Make sure all these endpoints exist and none have been clobbered
- for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
+ for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
+ MethodNamesMap('list_route_get', 'list_route_get'),
+ MethodNamesMap('list_route_post', 'list_route_post'),
+ MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
+ MethodNamesMap('detail_route_get', 'detail_route_get'),
+ MethodNamesMap('detail_route_post', 'detail_route_post')
+ ]):
route = decorator_routes[i]
# check url listing
- if endpoint.startswith('list_'):
+ method_name = endpoint.method_name
+ custom_method_name = endpoint.custom_method_name
+
+ if method_name.startswith('list_'):
self.assertEqual(route.url,
- '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name))
else:
self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name))
# check method to function mapping
- if endpoint.endswith('_post'):
+ if method_name.endswith('_post'):
method_map = 'post'
else:
method_map = 'get'
- self.assertEqual(route.mapping[method_map], endpoint)
+ self.assertEqual(route.mapping[method_map], method_name)
class TestRootWithAListlessViewset(TestCase):
From 92ebeaa040f75dbc6142355fa25d89b4c990685b Mon Sep 17 00:00:00 2001
From: tanwanirahul
Date: Fri, 19 Dec 2014 19:52:59 +0530
Subject: [PATCH 003/192] Change decorator attribute name to url_path per
suggestions
---
rest_framework/routers.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index d1c9fa1b9..a213f62c7 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -177,26 +177,26 @@ class SimpleRouter(BaseRouter):
# Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in detail_routes:
method_kwargs = getattr(viewset, methodname).kwargs
- custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname
+ url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, custom_method_name),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, custom_method_name),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes:
method_kwargs = getattr(viewset, methodname).kwargs
- custom_method_name = method_kwargs.pop("custom_method_name", None) or methodname
+ url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, custom_method_name),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, custom_method_name),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
else:
From 2448cc8e856369ca6fb99b848e10f8ff0105e925 Mon Sep 17 00:00:00 2001
From: tanwanirahul
Date: Fri, 19 Dec 2014 19:53:48 +0530
Subject: [PATCH 004/192] Updated tests to use url_path attribute in list and
detail decorators
---
tests/test_routers.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index d426f8320..73d10822a 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -261,11 +261,11 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
- @list_route(custom_method_name="list_custom-route")
+ @list_route(url_path="list_custom-route")
def list_custom_route_get(self, request, *args, **kwargs):
return Response({'method': 'link1'})
- @detail_route(custom_method_name="detail_custom-route")
+ @detail_route(url_path="detail_custom-route")
def detail_custom_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
@@ -278,7 +278,7 @@ class TestDynamicListAndDetailRouter(TestCase):
routes = self.router.get_routes(DynamicListAndDetailViewSet)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
- MethodNamesMap = namedtuple('MethodNamesMap', 'method_name custom_method_name')
+ MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
# Make sure all these endpoints exist and none have been clobbered
for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
MethodNamesMap('list_route_get', 'list_route_get'),
@@ -290,14 +290,14 @@ class TestDynamicListAndDetailRouter(TestCase):
route = decorator_routes[i]
# check url listing
method_name = endpoint.method_name
- custom_method_name = endpoint.custom_method_name
+ url_path = endpoint.url_path
if method_name.startswith('list_'):
self.assertEqual(route.url,
- '^{{prefix}}/{0}{{trailing_slash}}$'.format(custom_method_name))
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
else:
self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(custom_method_name))
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
# check method to function mapping
if method_name.endswith('_post'):
method_map = 'post'
From a8a3fedb5c52cc62c6ecf59c4138e9a6ecf04806 Mon Sep 17 00:00:00 2001
From: tanwanirahul
Date: Fri, 19 Dec 2014 20:16:46 +0530
Subject: [PATCH 005/192] Add url_path documention for detail_route decorator
---
docs/tutorial/6-viewsets-and-routers.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index cf37a2601..8e4e22f05 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
+The URLs for custom actions by default depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same.
+
## Binding ViewSets to URLs explicitly
The handler methods only get bound to the actions when we define the URLConf.
From 6aa0e307c99d0c17d7c48f2416472c7dbdcbbf8f Mon Sep 17 00:00:00 2001
From: Rahul
Date: Fri, 19 Dec 2014 20:31:21 +0530
Subject: [PATCH 006/192] Added documentation about url_path parameter for
custom actions.
---
docs/api-guide/routers.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 61a476b8b..63b8b59a8 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -68,6 +68,24 @@ The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
+If you did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` to customize it.
+
+For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
+
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import detail_route
+
+ class UserViewSet(ModelViewSet):
+ ...
+
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
+ def set_password(self, request, pk=None):
+ ...
+
+Above example would instead generate following URL pattern:
+
+* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
+
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
# API Guide
From b4a3e7f64096ea7106ff0d622bdf1c6e2e4e2895 Mon Sep 17 00:00:00 2001
From: Rahul
Date: Fri, 19 Dec 2014 21:20:19 +0530
Subject: [PATCH 007/192] Updates url_path info per suggestion
---
docs/api-guide/routers.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 63b8b59a8..87b6f15ac 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -68,7 +68,7 @@ The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
-If you did not like the default URL generated for your custom action, you could use `url_path` parameter with `@detail_route` or `@list_route` to customize it.
+If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
@@ -82,7 +82,7 @@ For example, if you want to change the URL for our custom action to `^users/{pk}
def set_password(self, request, pk=None):
...
-Above example would instead generate following URL pattern:
+The above example would now generate the following URL pattern:
* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
From 8f0fef4b75f5c999c13b5d37a263da3a3388142e Mon Sep 17 00:00:00 2001
From: Rahul
Date: Fri, 19 Dec 2014 21:22:10 +0530
Subject: [PATCH 008/192] Updated documentation on url_path per suggestions.
---
docs/tutorial/6-viewsets-and-routers.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index 8e4e22f05..d2ee11028 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -53,7 +53,7 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
-The URLs for custom actions by default depends on the method name itself. If you want to change the way url should be constructed, you can use `url_path` parameter of `@detail_route` and provide the string value for the same.
+The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument.
## Binding ViewSets to URLs explicitly
From 48d15f6ff8a13aafd5b4977c8d1b4b7fe70b4f6a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Dec 2014 16:58:35 +0000
Subject: [PATCH 009/192] 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 010/192] 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 77e3021fea3e30382b9770eac25371495e0b156b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 20 Dec 2014 16:26:51 +0000
Subject: [PATCH 011/192] Better behaviour with null and '' for blank HTML
fields.
---
rest_framework/fields.py | 13 +++++--------
tests/test_fields.py | 22 +++++++++++++++-------
2 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index c40dc3fb3..aab80982a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -273,7 +273,11 @@ class Field(object):
return empty
return self.default_empty_html
ret = dictionary[self.field_name]
- return self.default_empty_html if (ret == '') else ret
+ if ret == '' and self.allow_null:
+ # If the field is blank, and null is a valid value then
+ # determine if we should use null instead.
+ return '' if getattr(self, 'allow_blank', False) else None
+ return ret
return dictionary.get(self.field_name, empty)
def get_attribute(self, instance):
@@ -545,8 +549,6 @@ class CharField(Field):
'min_length': _('Ensure this field has at least {min_length} characters.')
}
initial = ''
- coerce_blank_to_null = False
- default_empty_html = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -560,11 +562,6 @@ class CharField(Field):
message = self.error_messages['min_length'].format(min_length=min_length)
self.validators.append(MinLengthValidator(min_length, message=message))
- if self.allow_null and (not self.allow_blank) and (self.default is empty):
- # HTML input cannot represent `None` values, so we need to
- # forcibly coerce empty HTML values to `None` if `allow_null=True`.
- self.default_empty_html = None
-
def run_validation(self, data=empty):
# Test for the empty string here so that it does not get validated,
# and so that subclasses do not need to handle it explicitly
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 04c721d36..775d46184 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -223,8 +223,8 @@ class MockHTMLDict(dict):
getlist = None
-class TestCharHTMLInput:
- def test_empty_html_checkbox(self):
+class TestHTMLInput:
+ def test_empty_html_charfield(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(default='happy')
@@ -232,23 +232,31 @@ class TestCharHTMLInput:
assert serializer.is_valid()
assert serializer.validated_data == {'message': 'happy'}
- def test_empty_html_checkbox_allow_null(self):
+ def test_empty_html_charfield_allow_null(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True)
- serializer = TestSerializer(data=MockHTMLDict())
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
assert serializer.is_valid()
assert serializer.validated_data == {'message': None}
- def test_empty_html_checkbox_allow_null_allow_blank(self):
+ def test_empty_html_datefield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ expiry = serializers.DateField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'expiry': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'expiry': None}
+
+ def test_empty_html_charfield_allow_null_allow_blank(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True, allow_blank=True)
- serializer = TestSerializer(data=MockHTMLDict({}))
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
assert serializer.is_valid()
assert serializer.validated_data == {'message': ''}
- def test_empty_html_required_false(self):
+ def test_empty_html_charfield_required_false(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(required=False)
From 03c4eb11305dcc9f366cdd008a5985bcf47c13ce Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sat, 20 Dec 2014 16:32:07 +0000
Subject: [PATCH 012/192] Use custom ListSerializer for pagination if one is
specified on the serializer.
---
rest_framework/pagination.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index fb4512854..f46b0dfa1 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -68,7 +68,12 @@ class BasePaginationSerializer(serializers.Serializer):
except AttributeError:
object_serializer = DefaultObjectSerializer
- self.fields[results_field] = serializers.ListSerializer(
+ try:
+ list_serializer_class = object_serializer.Meta.list_serializer_class
+ except AttributeError:
+ list_serializer_class = serializers.ListSerializer
+
+ self.fields[results_field] = list_serializer_class(
child=object_serializer(),
source='object_list'
)
From 35696748603665526be7947e918d41856644ec52 Mon Sep 17 00:00:00 2001
From: Brian Stearns
Date: Sun, 21 Dec 2014 18:53:35 -0500
Subject: [PATCH 013/192] use of double quotes broke the code highlighting.
---
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 f06db56cf..946e355da 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -480,7 +480,7 @@ Let's look at an example of serializing a class that represents an RGB color val
class ColorField(serializers.Field):
"""
- Color objects are serialized into "rgb(#, #, #)" notation.
+ Color objects are serialized into 'rgb(#, #, #)' notation.
"""
def to_representation(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
From 6c5ff712783ae7e6edebb52508f1d43249f1aa00 Mon Sep 17 00:00:00 2001
From: Remi Paulmier
Date: Mon, 22 Dec 2014 18:05:07 +0100
Subject: [PATCH 014/192] fix the way to use textarea rather than input with
models.TextField
---
rest_framework/utils/field_mapping.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index fca97b4b3..b16e9df08 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -80,7 +80,7 @@ def get_field_kwargs(field_name, model_field):
kwargs['decimal_places'] = decimal_places
if isinstance(model_field, models.TextField):
- kwargs['style'] = {'type': 'textarea'}
+ kwargs['style'] = {'base_template': 'textarea.html'}
if isinstance(model_field, models.AutoField) or not model_field.editable:
# If this field is read-only, then return early.
From 18687f075d9fb998b82c6fb8f6cb37eb1ed7e5bf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mat=C3=ADas=20Lang?=
Date: Tue, 23 Dec 2014 12:22:10 -0300
Subject: [PATCH 015/192] Documented an optional argument of
HyperlinkedIdentityField
lookup_url_kwarg argument of HyperlinkedIdentityField wasn't documented
---
docs/api-guide/relations.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index e56db229a..50e3b7b59 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -231,6 +231,7 @@ This field is always read-only.
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `-detail`. **required**.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
+* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
---
From 399cb165b0ba26df6052c114eb77961dc578e686 Mon Sep 17 00:00:00 2001
From: Andrew Seier
Date: Tue, 23 Dec 2014 12:11:45 -0800
Subject: [PATCH 016/192] Remove commented code (warning during compression)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
manage.py compress —force causes a warning here.
---
.../templates/rest_framework/vertical/list_fieldset.html | 4 ----
1 file changed, 4 deletions(-)
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
index 1d86c7f2b..82d7b5f41 100644
--- a/rest_framework/templates/rest_framework/vertical/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -1,8 +1,4 @@
From 35768344db45b9fa6bd94c3fd48d5e232027434e Mon Sep 17 00:00:00 2001
From: Andrew Seier
Date: Tue, 23 Dec 2014 12:12:22 -0800
Subject: [PATCH 017/192] =?UTF-8?q?Remove=20=E2=80=98/=E2=80=98=20from=20i?=
=?UTF-8?q?nside=20variable=20block=20{{=20}}?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
manage.py compress —force causes a warning here.
---
.../templates/rest_framework/inline/checkbox_multiple.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
index 6caf64403..093496862 100644
--- a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
+++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
@@ -5,7 +5,7 @@
{% for key, text in field.choices.items %}
From b32ecdefbace063c5b9b465af608ac6404795dd4 Mon Sep 17 00:00:00 2001
From: Remi Paulmier
Date: Wed, 24 Dec 2014 14:07:28 +0100
Subject: [PATCH 018/192] modified the tests accordingly
---
tests/test_model_serializer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index da79164af..ee556dbcb 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -119,7 +119,7 @@ class TestRegularFieldMappings(TestCase):
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
small_integer_field = IntegerField()
- text_field = CharField(style={'type': 'textarea'})
+ text_field = CharField(style={'base_template': 'textarea.html'})
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=)
From c2e00a075cb4b44c644ad5d62f2be0fd19e62c5f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Dec 2014 15:25:13 +0000
Subject: [PATCH 019/192] Paginated serializers should get context.
---
rest_framework/pagination.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index f46b0dfa1..f31e5fa4c 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -77,6 +77,7 @@ class BasePaginationSerializer(serializers.Serializer):
child=object_serializer(),
source='object_list'
)
+ self.fields[results_field].bind(field_name=results_field, parent=self)
class PaginationSerializer(BasePaginationSerializer):
From 00531ec937206e7e0af949c67872c915d0752b5a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Dec 2014 15:48:16 +0000
Subject: [PATCH 020/192] Release notes on non-text detail arguments. Closes
#2341.
---
docs/topics/3.0-announcement.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 0710766f7..68d247827 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -940,6 +940,7 @@ The default JSON renderer will return float objects for un-coerced `Decimal` ins
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
+* `APIException` subclasses could previously take could previously take any arbitrary type in the `detail` argument. These exceptions now use translatable text strings, and as a result call `force_text` on the `detail` argument, which *must be a string*. If you need complex arguments to an `APIException` class, you should subclass it and override the `__init__()` method. Typically you'll instead want to use a custom exception handler to provide for non-standard error responses.
---
From 5b5652594a9c000d8e925d35efa03be27c28c077 Mon Sep 17 00:00:00 2001
From: Rocky Meza
Date: Fri, 26 Dec 2014 22:24:31 -0700
Subject: [PATCH 021/192] Typo manger => manager
---
docs/api-guide/serializers.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index b9f0e7bc0..f88ec51f2 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -384,7 +384,7 @@ This manager class now more nicely encapsulates that user instances and profile
has_support_contract=validated_data['profile']['has_support_contract']
)
-For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manger classes](encapsulation-blogpost).
+For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost).
## Dealing with multiple objects
From a636320ff3b381a6d7d8685f1b4fba8bdd6c8b94 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 11:02:19 +0000
Subject: [PATCH 022/192] Add import notes in docs. Closes #2357
---
docs/api-guide/generic-views.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index f5bbdfdda..6374e3052 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -214,6 +214,8 @@ You won't typically need to override the following methods, although you might n
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
+The mixin classes can be imported from `rest_framework.mixins`.
+
## ListModelMixin
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
@@ -258,6 +260,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
+The view classes can be imported from `rest_framework.generics`.
+
## CreateAPIView
Used for **create-only** endpoints.
From ef2eff2abac64ffbed621bb9a72a2229841a1db1 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 11:07:38 +0000
Subject: [PATCH 023/192] Only pass max_length for CharField. Closes #2317.
---
rest_framework/utils/field_mapping.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index b16e9df08..b2f4dd80e 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -106,7 +106,7 @@ def get_field_kwargs(field_name, model_field):
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
- if max_length is not None:
+ if max_length is not None and isinstance(model_field, models.CharField):
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg
From 7b42c5ed17a2430d66da88932ad4e81492d9b914 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 11:14:32 +0000
Subject: [PATCH 024/192] Remove broken test. Closes #2359.
---
tests/test_routers.py | 16 ----------------
1 file changed, 16 deletions(-)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 06ab8103a..2b6cd7d28 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -305,19 +305,3 @@ class TestDynamicListAndDetailRouter(TestCase):
else:
method_map = 'get'
self.assertEqual(route.mapping[method_map], method_name)
-
-
-class TestRootWithAListlessViewset(TestCase):
- def setUp(self):
- class NoteViewSet(mixins.RetrieveModelMixin,
- viewsets.GenericViewSet):
- model = RouterTestModel
-
- self.router = DefaultRouter()
- self.router.register(r'notes', NoteViewSet)
- self.view = self.router.urls[0].callback
-
- def test_api_root(self):
- request = factory.get('/')
- response = self.view(request)
- self.assertEqual(response.data, {})
From 8dc95ee22181de6e38c7187426bca9fcee9d7927 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 11:24:49 +0000
Subject: [PATCH 025/192] Add notes on include and namespacing. Closes #2335.
---
docs/api-guide/routers.md | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 6819adb6a..3a8a8f6cd 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -49,6 +49,38 @@ This means you'll need to explicitly set the `base_name` argument when registeri
---
+### Using `include` with routers
+
+The `.urls` attribute on a router instance is simply a standard list of URL patterns. There are a number of different styles for how you can include these URLs.
+
+For example, you can append `router.urls` to a list of existing views…
+
+ router = routers.SimpleRouter()
+ router.register(r'users', UserViewSet)
+ router.register(r'accounts', AccountViewSet)
+
+ urlpatterns = [
+ url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
+ ]
+
+ urlpatterns += router.urls
+
+Alternatively you can use Django's `include` function, like so…
+
+ urlpatterns = [
+ 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'))
+ ]
+
+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.
+
### Extra link and actions
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
From 67fc002f91e5dc617dab45895ded32d6be6c2a40 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 11:26:38 +0000
Subject: [PATCH 026/192] Drop unused import
---
tests/test_routers.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 2b6cd7d28..fc22a8d95 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -3,7 +3,7 @@ from django.conf.urls import patterns, url, include
from django.db import models
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
-from rest_framework import serializers, viewsets, mixins, permissions
+from rest_framework import serializers, viewsets, permissions
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
From efa5942ce1c5d2286fd91994b52fb73a5690426c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 12:02:52 +0000
Subject: [PATCH 027/192] Support namespaced router URLs with DefaultRouter.
---
rest_framework/compat.py | 10 +++++
rest_framework/routers.py | 5 ++-
tests/test_routers.py | 94 ++++++++++++++++++++++++++-------------
3 files changed, 76 insertions(+), 33 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 69fdd7936..ba26a3cd7 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -50,6 +50,16 @@ except ImportError:
from django.http import HttpResponse as HttpResponseBase
+# request only provides `resolver_match` from 1.5 onwards.
+def get_resolver_match(request):
+ try:
+ return request.resolver_match
+ except AttributeError:
+ # Django < 1.5
+ from django.core.urlresolvers import resolve
+ return resolve(request.path_info)
+
+
# django-filter is optional
try:
import django_filters
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 1cb65b1c0..61f3ccab0 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -21,7 +21,7 @@ from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
-from rest_framework.compat import OrderedDict
+from rest_framework.compat import get_resolver_match, OrderedDict
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
@@ -292,7 +292,10 @@ class DefaultRouter(SimpleRouter):
def get(self, request, *args, **kwargs):
ret = OrderedDict()
+ namespace = get_resolver_match(request).namespace
for key, url_name in api_root_dict.items():
+ if namespace:
+ url_name = namespace + ':' + url_name
try:
ret[key] = reverse(
url_name,
diff --git a/tests/test_routers.py b/tests/test_routers.py
index fc22a8d95..86113f5d7 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -1,5 +1,5 @@
from __future__ import unicode_literals
-from django.conf.urls import patterns, url, include
+from django.conf.urls import url, include
from django.db import models
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
@@ -12,7 +12,42 @@ from collections import namedtuple
factory = APIRequestFactory()
-urlpatterns = patterns('',)
+
+class RouterTestModel(models.Model):
+ uuid = models.CharField(max_length=20)
+ text = models.CharField(max_length=200)
+
+
+class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
+
+ class Meta:
+ model = RouterTestModel
+ fields = ('url', 'uuid', 'text')
+
+
+class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ serializer_class = NoteSerializer
+ lookup_field = 'uuid'
+
+
+class MockViewSet(viewsets.ModelViewSet):
+ queryset = None
+ serializer_class = None
+
+
+notes_router = SimpleRouter()
+notes_router.register(r'notes', NoteViewSet)
+
+namespaced_router = DefaultRouter()
+namespaced_router.register(r'example', MockViewSet, base_name='example')
+
+urlpatterns = [
+ url(r'^non-namespaced/', include(namespaced_router.urls)),
+ url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
+ url(r'^example/', include(notes_router.urls)),
+]
class BasicViewSet(viewsets.ViewSet):
@@ -64,9 +99,26 @@ class TestSimpleRouter(TestCase):
self.assertEqual(route.mapping[method], endpoint)
-class RouterTestModel(models.Model):
- uuid = models.CharField(max_length=20)
- text = models.CharField(max_length=200)
+class TestRootView(TestCase):
+ urls = 'tests.test_routers'
+
+ def test_retrieve_namespaced_root(self):
+ response = self.client.get('/namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/namespaced/example/",
+ }
+ )
+
+ def test_retrieve_non_namespaced_root(self):
+ response = self.client.get('/non-namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/non-namespaced/example/",
+ }
+ )
class TestCustomLookupFields(TestCase):
@@ -76,51 +128,29 @@ class TestCustomLookupFields(TestCase):
urls = 'tests.test_routers'
def setUp(self):
- class NoteSerializer(serializers.HyperlinkedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
-
- class Meta:
- model = RouterTestModel
- fields = ('url', 'uuid', 'text')
-
- class NoteViewSet(viewsets.ModelViewSet):
- queryset = RouterTestModel.objects.all()
- serializer_class = NoteSerializer
- lookup_field = 'uuid'
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
-
- from tests import test_routers
- urls = getattr(test_routers, 'urlpatterns')
- urls += patterns(
- '',
- url(r'^', include(self.router.urls)),
- )
-
RouterTestModel.objects.create(uuid='123', text='foo bar')
def test_custom_lookup_field_route(self):
- detail_route = self.router.urls[-1]
+ detail_route = notes_router.urls[-1]
detail_url_pattern = detail_route.regex.pattern
self.assertIn('', detail_url_pattern)
def test_retrieve_lookup_field_list_view(self):
- response = self.client.get('/notes/')
+ response = self.client.get('/example/notes/')
self.assertEqual(
response.data,
[{
- "url": "http://testserver/notes/123/",
+ "url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}]
)
def test_retrieve_lookup_field_detail_view(self):
- response = self.client.get('/notes/123/')
+ response = self.client.get('/example/notes/123/')
self.assertEqual(
response.data,
{
- "url": "http://testserver/notes/123/",
+ "url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}
)
From d8e66970a11ec2d4b66f0cf56950f2cc83e83224 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 12:14:07 +0000
Subject: [PATCH 028/192] Note on using i18n_patterns with
format_suffix_patterns. Closes #2278.
---
docs/api-guide/format-suffixes.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md
index 20c1e9952..35dbcd39c 100644
--- a/docs/api-guide/format-suffixes.md
+++ b/docs/api-guide/format-suffixes.md
@@ -55,6 +55,18 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
+### Using with `i18n_patterns`
+
+If using the `i18n_patterns` function provided by Django, as well as `format_suffix_patterns` you should make sure that the `i18n_patterns` function is applied as the final, or outermost function. For example:
+
+ url patterns = [
+ …
+ ]
+
+ urlpatterns = i18n_patterns(
+ format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
+ )
+
---
## Accept headers vs. format suffixes
From 5d8c45681a945b955d9336b0fd1e4ebccf0df895 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Sun, 28 Dec 2014 18:48:42 +0000
Subject: [PATCH 029/192] Update copryright for 2015. Closes #2247.
---
README.md | 2 +-
docs/index.md | 2 +-
rest_framework/__init__.py | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index df0a4086a..8fc11c30f 100644
--- a/README.md
+++ b/README.md
@@ -156,7 +156,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
# License
-Copyright (c) 2011-2014, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/docs/index.md b/docs/index.md
index 8a96fc9fb..55129df18 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -235,7 +235,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
## License
-Copyright (c) 2011-2014, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 6808b74b0..dec89b3e9 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -11,7 +11,7 @@ __title__ = 'Django REST framework'
__version__ = '3.0.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
-__copyright__ = 'Copyright 2011-2014 Tom Christie'
+__copyright__ = 'Copyright 2011-2015 Tom Christie'
# Version synonym
VERSION = __version__
From a7479721c844926f377085d8c336a2f60b7b2a38 Mon Sep 17 00:00:00 2001
From: Kyle Valade
Date: Mon, 29 Dec 2014 00:35:00 -0800
Subject: [PATCH 030/192] First pass at refactoring get_field_info in
utils.model_meta
---
rest_framework/utils/model_meta.py | 57 ++++++++++++++++++++++--------
1 file changed, 43 insertions(+), 14 deletions(-)
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index c98725c66..375d2e8c6 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -35,7 +35,7 @@ def _resolve_model(obj):
Resolve supplied `obj` to a Django model class.
`obj` must be a Django model class itself, or a string
- representation of one. Useful in situtations like GH #1225 where
+ representation of one. Useful in situations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
@@ -56,23 +56,44 @@ def _resolve_model(obj):
def get_field_info(model):
"""
- Given a model class, returns a `FieldInfo` instance containing metadata
- about the various field types on the model.
+ Given a model class, returns a `FieldInfo` instance, which is a
+ `namedtuple`, containing metadata about the various field types on the model
+ including information about their relationships.
"""
opts = model._meta.concrete_model._meta
- # Deal with the primary key.
+ pk = _get_pk(opts)
+ fields = _get_fields(opts)
+ forward_relations = _get_forward_relationships(opts)
+ reverse_relations = _get_reverse_relationships(opts)
+ fields_and_pk = _merge_fields_and_pk(pk, fields)
+ relationships = _merge_relationships(forward_relations, reverse_relations)
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations,
+ fields_and_pk, relationships)
+
+
+def _get_pk(opts):
pk = opts.pk
while pk.rel and pk.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk.
+ # If model is a child via multi-table inheritance, use parent's pk.
pk = pk.rel.to._meta.pk
- # Deal with regular fields.
+ return pk
+
+
+def _get_fields(opts):
fields = OrderedDict()
for field in [field for field in opts.fields if field.serialize and not field.rel]:
fields[field.name] = field
- # Deal with forward relationships.
+ return fields
+
+
+def _get_forward_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
forward_relations = OrderedDict()
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
@@ -93,7 +114,13 @@ def get_field_info(model):
)
)
- # Deal with reverse relationships.
+ return forward_relations
+
+
+def _get_reverse_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
reverse_relations = OrderedDict()
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
@@ -117,18 +144,20 @@ def get_field_info(model):
)
)
- # Shortcut that merges both regular fields and the pk,
- # for simplifying regular field lookup.
+ return reverse_relations
+
+
+def _merge_fields_and_pk(pk, fields):
fields_and_pk = OrderedDict()
fields_and_pk['pk'] = pk
fields_and_pk[pk.name] = pk
fields_and_pk.update(fields)
- # Shortcut that merges both forward and reverse relationships
+ return fields_and_pk
- relations = OrderedDict(
+
+def _merge_relationships(forward_relations, reverse_relations):
+ return OrderedDict(
list(forward_relations.items()) +
list(reverse_relations.items())
)
-
- return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
From 336faf5a861fad2e4364a68fbf0747bef4457358 Mon Sep 17 00:00:00 2001
From: Rob Terhaar
Date: Thu, 1 Jan 2015 21:01:16 -0500
Subject: [PATCH 031/192] fix widget style formatting
---
docs/tutorial/1-serialization.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index ff507a2b8..60a3d9897 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -124,7 +124,7 @@ The first part of the serializer class defines the fields that get serialized/de
A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
-The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
+The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `{'base_template': 'textarea.html'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
@@ -206,7 +206,7 @@ One nice property that serializers have is that you can inspect all the fields i
SnippetSerializer():
id = IntegerField(label='ID', read_only=True)
title = CharField(allow_blank=True, max_length=100, required=False)
- code = CharField(style={'type': 'textarea'})
+ code = CharField(style={'base_template': 'textarea.html'})
linenos = BooleanField(required=False)
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
From 309b5d264166e07510d6cbc54d681268d07957aa Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Fri, 2 Jan 2015 11:07:35 +0000
Subject: [PATCH 032/192] 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 033/192] 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 034/192] 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 035/192] 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 036/192] 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 037/192] 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 038/192] 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 039/192] 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 040/192] 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 8cf37449715c32c4a692667814466c7f32e8734f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 10:52:18 +0000
Subject: [PATCH 041/192] Ensure no invalid min_length/min_value/max_value
arguments. Closes #2369.
---
rest_framework/utils/field_mapping.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index b2f4dd80e..cba40d318 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -10,6 +10,11 @@ from rest_framework.validators import UniqueValidator
import inspect
+NUMERIC_FIELD_TYPES = (
+ models.IntegerField, models.FloatField, models.DecimalField
+)
+
+
class ClassLookupDict(object):
"""
Takes a dictionary with classes as keys.
@@ -119,7 +124,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MinLengthValidator)
), None)
- if min_length is not None:
+ if min_length is not None and isinstance(model_field, models.CharField):
kwargs['min_length'] = min_length
validator_kwarg = [
validator for validator in validator_kwarg
@@ -132,7 +137,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MaxValueValidator)
), None)
- if max_value is not None:
+ if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
kwargs['max_value'] = max_value
validator_kwarg = [
validator for validator in validator_kwarg
@@ -145,7 +150,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MinValueValidator)
), None)
- if min_value is not None:
+ if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
kwargs['min_value'] = min_value
validator_kwarg = [
validator for validator in validator_kwarg
From 17665aa52a9cd5599099c19fd8f54540a5d436ce Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 12:26:15 +0000
Subject: [PATCH 042/192] Add docs for OAuth, XML, YAML, JSONP packages. Closes
#2179.
---
README.md | 4 +-
docs/api-guide/authentication.md | 47 +++++++++++++++++--
docs/api-guide/parsers.md | 51 ++++++++++++++++++--
docs/api-guide/renderers.md | 80 +++++++++++++++++++++++++++++++-
docs/index.md | 9 ++--
5 files changed, 174 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 4c9d765ea..6742a7b1d 100644
--- a/README.md
+++ b/README.md
@@ -190,8 +190,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[sandbox]: http://restframework.herokuapp.com/
[index]: http://www.django-rest-framework.org/
-[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication
-[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers
[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer
[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 2074f1bfc..bb7318177 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -293,14 +293,49 @@ The following example will authenticate any incoming request as the user given b
The following third party packages are also available.
+## Django OAuth Toolkit
+
+The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
+
+#### Installation & configuration
+
+Install using `pip`.
+
+ pip install django-oauth-toolkit
+
+Add the package to your `INSTALLED_APPS` and modify your REST framework settings.
+
+ INSTALLED_APPS = (
+ ...
+ 'oauth2_provider',
+ )
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'oauth2_provider.ext.rest_framework.OAuth2Authentication',
+ )
+ }
+
+For more details see the [Django REST framework - Getting started][django-oauth-toolkit-getting-started] documentation.
+
+## Django REST framework OAuth
+
+The [Django REST framework OAuth][django-rest-framework-oauth] package provides both OAuth1 and OAuth2 support for REST framework.
+
+This package was previously included directly in REST framework but is now supported and maintained as a third party package.
+
+#### Installation & configuration
+
+Install the package using `pip`.
+
+ pip install djangorestframework-oauth
+
+For details on configuration and usage see the Django REST framework OAuth documentation for [authentication][django-rest-framework-oauth-authentication] and [permissions][django-rest-framework-oauth-permissions].
+
## Digest Authentication
HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
-## Django OAuth Toolkit
-
-The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
-
## Django OAuth2 Consumer
The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
@@ -332,6 +367,10 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization
[custom-user-model]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model
[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html
+[django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.org/en/latest/rest-framework/getting_started.html
+[django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/
+[django-rest-framework-oauth-authentication]: http://jpadilla.github.io/django-rest-framework-oauth/authentication/
+[django-rest-framework-oauth-permissions]: http://jpadilla.github.io/django-rest-framework-oauth/permissions/
[juanriaza]: https://github.com/juanriaza
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
[oauth-1.0a]: http://oauth.net/core/1.0a
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 9323d3822..b68b33be9 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -26,7 +26,7 @@ As an example, if you are sending `json` encoded data using jQuery with the [.aj
## Setting the parsers
-The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `JSON` content.
+The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow only requests with `JSON` content, instead of the default of JSON or form data.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
@@ -37,8 +37,8 @@ The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSE
You can also set the parsers used for an individual view, or viewset,
using the `APIView` class based views.
- from rest_framework.parsers import JSONParser
- from rest_framework.response import Response
+ from rest_framework.parsers import JSONParser
+ from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
@@ -162,6 +162,48 @@ The following is an example plaintext parser that will populate the `request.dat
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
@@ -173,6 +215,9 @@ The following third party packages are also available.
[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
[juanriaza]: https://github.com/juanriaza
[vbabiy]: https://github.com/vbabiy
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 69460dbc5..83ded849d 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -342,13 +342,81 @@ Templates will render with a `RequestContext` which includes the `status_code` a
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
+## JSONP
+
+[REST framework JSONP][rest-framework-jsonp] provides JSONP rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+---
+
+**Warning**: If you require cross-domain AJAX requests, you should generally be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
+
+The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
+
+---
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-jsonp
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.JSONPRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
## CSV
-Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
+Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
## UltraJSON
@@ -358,7 +426,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
-
## Pandas (CSV, Excel, PNG)
[Django REST Pandas] provides a serializer and renderers that support additional data processing and output via the [Pandas] DataFrame API. Django REST Pandas includes renderers for Pandas-style CSV files, Excel workbooks (both `.xls` and `.xlsx`), and a number of [other formats]. It is maintained by [S. Andrew Sheppard][sheppard] as part of the [wq Project][wq].
@@ -373,10 +440,19 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[application/vnd.github+json]: http://developer.github.com/v3/media/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
+[rest-framework-jsonp]: http://jpadilla.github.io/django-rest-framework-jsonp/
+[cors]: http://www.w3.org/TR/cors/
+[cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/
+[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
[messagepack]: http://msgpack.org/
[juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu
[vbabiy]: https://github.com/vbabiy
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
[ultrajson]: https://github.com/esnme/ultrajson
diff --git a/docs/index.md b/docs/index.md
index 7ccec12fe..544204c65 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -28,13 +28,12 @@ For more details see the [3.0 release notes][3.0-announcement].
-
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
Some reasons you might want to use REST framework:
* The [Web browsable API][sandbox] is a huge usability win for your developers.
-* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
+* [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
@@ -57,7 +56,6 @@ The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
-* [django-restframework-oauth][django-restframework-oauth] package for OAuth 1.0a and 2.0 support.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
## Installation
@@ -260,13 +258,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
[django-filter]: http://pypi.python.org/pypi/django-filter
-[django-restframework-oauth]: https://github.com/jlafon/django-rest-framework-oauth
[django-guardian]: https://github.com/lukaszb/django-guardian
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[index]: .
-[oauth1-section]: api-guide/authentication#oauthauthentication
-[oauth2-section]: api-guide/authentication#oauth2authentication
+[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: api-guide/authentication/#django-oauth-toolkit
[serializer-section]: api-guide/serializers#serializers
[modelserializer-section]: api-guide/serializers#modelserializer
[functionview-section]: api-guide/views#function-based-views
From b6ca7248ebcf95a95e1911aa0b130f653b8bf690 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 14:32:12 +0000
Subject: [PATCH 043/192] required=False allows omission of value for output.
Closes #2342
---
docs/api-guide/fields.md | 2 ++
rest_framework/fields.py | 2 ++
rest_framework/serializers.py | 8 ++++-
tests/test_serializer.py | 62 +++++++++++++++++++++++++++++++++++
4 files changed, 73 insertions(+), 1 deletion(-)
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 946e355da..b3d274ddb 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -41,6 +41,8 @@ Defaults to `False`
Normally an error will be raised if a field is not supplied during deserialization.
Set to false if this field is not required to be present during deserialization.
+Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation.
+
Defaults to `True`.
### `allow_null`
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index aab80982a..cc9410aa7 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -288,6 +288,8 @@ class Field(object):
try:
return get_attribute(instance, self.source_attrs)
except (KeyError, AttributeError) as exc:
+ if not self.required and self.default is empty:
+ raise SkipField()
msg = (
'Got {exc_type} when attempting to get a value for field '
'`{field}` on serializer `{serializer}`.\nThe serializer '
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 6f89df0db..53f092d7a 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -419,8 +419,14 @@ class Serializer(BaseSerializer):
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
- attribute = field.get_attribute(instance)
+ try:
+ attribute = field.get_attribute(instance)
+ except SkipField:
+ continue
+
if attribute is None:
+ # We skip `to_representation` for `None` values so that
+ # fields do not have to explicitly deal with that case.
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index c17b6d8c5..68bbbe983 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,5 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
+from .utils import MockObject
from rest_framework import serializers
from rest_framework.compat import unicode_repr
import pytest
@@ -216,3 +217,64 @@ class TestUnicodeRepr:
instance = ExampleObject()
serializer = ExampleSerializer(instance)
repr(serializer) # Should not error.
+
+
+class TestNotRequiredOutput:
+ def test_not_required_output_for_dict(self):
+ """
+ 'required=False' should allow a dictionary key to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_not_required_output_for_object(self):
+ """
+ 'required=False' should allow an object attribute to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ def create(self, validated_data):
+ return MockObject(**validated_data)
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ serializer.save()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_default_required_output_for_dict(self):
+ """
+ 'default="something"' should require dictionary key.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer({'included': 'abc'})
+ with pytest.raises(KeyError):
+ serializer.data
+
+ def test_default_required_output_for_object(self):
+ """
+ 'default="something"' should require object attribute.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ instance = MockObject(included='abc')
+ serializer = ExampleSerializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
From 49dc037a961b618baf8eb189b094633238867b41 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 15:03:09 +0000
Subject: [PATCH 044/192] 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 6fd33ddea9e5b8f9e979e573a27873131846ea48 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 15:04:01 +0000
Subject: [PATCH 045/192] Udpate 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 53f092d7a..e373cd107 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 26ac2656e5e0b3d01a67551910113a305d2a2820 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 16:20:15 +0000
Subject: [PATCH 046/192] Pass init arguments through to serializer from
pagination serializer.
Closes #2355.
Normally a serializer won't need these arguments on __init__, but
if a user has customized __init__ they may expect them to be available.
---
rest_framework/pagination.py | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index f31e5fa4c..9c8dda8f9 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -37,16 +37,13 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page)
-class DefaultObjectSerializer(serializers.ReadOnlyField):
+class DefaultObjectSerializer(serializers.Serializer):
"""
If no object serializer is specified, then this serializer will be applied
as the default.
"""
-
- 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)
+ def to_representation(self, value):
+ return value
class BasePaginationSerializer(serializers.Serializer):
@@ -74,10 +71,9 @@ class BasePaginationSerializer(serializers.Serializer):
list_serializer_class = serializers.ListSerializer
self.fields[results_field] = list_serializer_class(
- child=object_serializer(),
+ child=object_serializer(*args, **kwargs),
source='object_list'
)
- self.fields[results_field].bind(field_name=results_field, parent=self)
class PaginationSerializer(BasePaginationSerializer):
From d3b2302588f333b22d5e4aa2be6eca0e944e9494 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 5 Jan 2015 16:31:52 +0000
Subject: [PATCH 047/192] Minor docs update. Refs #2375.
---
docs/topics/3.0-announcement.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 68d247827..5dbc5600a 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -87,12 +87,12 @@ The resulting API changes are further detailed below.
#### The `.create()` and `.update()` methods.
-The `.restore_object()` method is now replaced with two separate methods, `.create()` and `.update()`.
-
-These methods also replace the optional `.save_object()` method, which no longer exists.
+The `.restore_object()` method is now removed, and we instead have two separate methods, `.create()` and `.update()`. These methods work slightly different to the previous `.restore_object()`.
When using the `.create()` and `.update()` methods you should both create *and save* the object instance. This is in contrast to the previous `.restore_object()` behavior that would instantiate the object but not save it.
+These methods also replace the optional `.save_object()` method, which no longer exists.
+
The following example from the tutorial previously used `restore_object()` to handle both creating and updating object instances.
def restore_object(self, attrs, instance=None):
From 271b638df10c0cf498cbc69847f388e978c4da78 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 6 Jan 2015 11:21:58 +0000
Subject: [PATCH 048/192] Update exception docs. Closes #2378.
---
docs/api-guide/exceptions.md | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index 467ad9709..993134f7f 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -18,7 +18,7 @@ The handled exceptions are:
In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error.
-By default all error responses will include a key `detail` in the body of the response, but other keys may also be included.
+Most error responses will include a key `detail` in the body of the response.
For example, the following request:
@@ -33,6 +33,16 @@ Might receive an error response indicating that the `DELETE` method is not allow
{"detail": "Method 'DELETE' not allowed."}
+Validation errors are handled slightly differently, and will include the field names as the keys in the response. If the validation error was not specific to a particular field then it will use the "non_field_errors" key, or whatever string value has been set for the `NON_FIELD_ERRORS_KEY` setting.
+
+Any example validation error might look like this:
+
+ HTTP/1.1 400 Bad Request
+ Content-Type: application/json
+ Content-Length: 94
+
+ {"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
+
## Custom exception handling
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.
From 07ad0474c0cef8f8e88d299eca9dffbe6d01c10d Mon Sep 17 00:00:00 2001
From: Ryan Gaffney
Date: Tue, 6 Jan 2015 14:34:36 -0800
Subject: [PATCH 049/192] Fix compatibility comment regarding OrderedDict
---
rest_framework/compat.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index ba26a3cd7..b1f6f2fa6 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -36,7 +36,7 @@ def unicode_to_repr(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.
-# For Django <= 1.6 and Python 2.6 fall back to OrderedDict.
+# For Django <= 1.6 and Python 2.6 fall back to SortedDict.
try:
from collections import OrderedDict
except ImportError:
From fe92a2cfee9e3a20e913500802d98a15e8b70780 Mon Sep 17 00:00:00 2001
From: JocelynDelalande
Date: Wed, 7 Jan 2015 10:42:11 +0100
Subject: [PATCH 050/192] fixed doc : DEFAULT_AUTHENTICATION_CLASSES ->
DEFAULT_AUTHENTICATION
+ It is consistent with docs about DEFAULT_PERMISSION_CLASSES
---
docs/api-guide/authentication.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index b04858e39..1222dbf04 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -34,7 +34,7 @@ The value of `request.user` and `request.auth` for unauthenticated requests can
## Setting the authentication scheme
-The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
+The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION_CLASSES` setting. For example.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
@@ -282,7 +282,7 @@ This authentication class depends on the optional [django-oauth2-provider][djang
'provider.oauth2',
)
-Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting:
+Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION_CLASSES` setting:
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.OAuth2Authentication',
From 7913947757b0e6bd1b8828db81933c32c498e20a Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 11:13:03 +0000
Subject: [PATCH 051/192] 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 052/192] 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 053/192] 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 054/192] 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 055/192] 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 056/192] 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 057/192] 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 058/192] 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 42f1932b520a90ae6f8e11246a0992e5f8983bd7 Mon Sep 17 00:00:00 2001
From: Xavier Ordoquy
Date: Wed, 7 Jan 2015 19:10:22 +0100
Subject: [PATCH 059/192] Release notes for 3.0.3
---
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 b9216e36f..18a47b71b 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,26 @@ You can determine your currently installed version using `pip freeze`:
## 3.0.x series
+
+### 3.0.3
+
+**Date**: [8th January 2015][3.0.3-milestone].
+
+* Fix `MinValueValidator` on `models.DateField`. ([#2369][gh2369])
+* Fix serializer missing context when pagination is used. ([#2355][gh2355])
+* Namespaced router URLs are now supported by the `DefaultRouter`. ([#2351][gh2351])
+* `required=False` allows omission of value for output. ([#2342][gh2342])
+* Use textarea input for `models.TextField`. ([#2340][gh2340])
+* Use custom `ListSerializer` for pagination if required. ([#2331][gh2331], [#2327][gh2327])
+* Better behavior with null and '' for blank HTML fields. ([#2330][gh2330])
+* Ensure fields in `exclude` are model fields. ([#2319][gh2319])
+* Fix `IntegerField` and `max_length` argument incompatibility. ([#2317][gh2317])
+* Fix the YAML encoder for 3.0 serializers. ([#2315][gh2315], [#2283][gh2283])
+* Fix the behavior of empty HTML fields. ([#2311][gh2311], [#1101][gh1101])
+* Fix Metaclass attribute depth ignoring fields attribute. ([#2287][gh2287])
+* Fix `format_suffix_patterns` to work with Django's `i18n_patterns`. ([#2278][gh2278])
+* Ability to customize router URLs for custom actions, using `url_path`. ([#2010][gh2010])
+
### 3.0.2
**Date**: [17th December 2014][3.0.2-milestone].
@@ -729,3 +749,21 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[gh2290]: https://github.com/tomchristie/django-rest-framework/issues/2290
[gh2291]: https://github.com/tomchristie/django-rest-framework/issues/2291
[gh2294]: https://github.com/tomchristie/django-rest-framework/issues/2294
+
+[gh1101]: https://github.com/tomchristie/django-rest-framework/issues/1101
+[gh2010]: https://github.com/tomchristie/django-rest-framework/issues/2010
+[gh2278]: https://github.com/tomchristie/django-rest-framework/issues/2278
+[gh2283]: https://github.com/tomchristie/django-rest-framework/issues/2283
+[gh2287]: https://github.com/tomchristie/django-rest-framework/issues/2287
+[gh2311]: https://github.com/tomchristie/django-rest-framework/issues/2311
+[gh2315]: https://github.com/tomchristie/django-rest-framework/issues/2315
+[gh2317]: https://github.com/tomchristie/django-rest-framework/issues/2317
+[gh2319]: https://github.com/tomchristie/django-rest-framework/issues/2319
+[gh2327]: https://github.com/tomchristie/django-rest-framework/issues/2327
+[gh2330]: https://github.com/tomchristie/django-rest-framework/issues/2330
+[gh2331]: https://github.com/tomchristie/django-rest-framework/issues/2331
+[gh2340]: https://github.com/tomchristie/django-rest-framework/issues/2340
+[gh2342]: https://github.com/tomchristie/django-rest-framework/issues/2342
+[gh2351]: https://github.com/tomchristie/django-rest-framework/issues/2351
+[gh2355]: https://github.com/tomchristie/django-rest-framework/issues/2355
+[gh2369]: https://github.com/tomchristie/django-rest-framework/issues/2369
From b7015ea8989d67617d276829ccb6a192362ee01f Mon Sep 17 00:00:00 2001
From: Xavier Ordoquy
Date: Wed, 7 Jan 2015 19:11:17 +0100
Subject: [PATCH 060/192] Bumped the version to 3.0.3.
---
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 dec89b3e9..fdcebb7b7 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '3.0.2'
+__version__ = '3.0.3'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2015 Tom Christie'
From 60f5b5d9f364c383662fb6ae8d210f31e9621c09 Mon Sep 17 00:00:00 2001
From: Xavier Ordoquy
Date: Wed, 7 Jan 2015 19:19:33 +0100
Subject: [PATCH 061/192] Make Django REST Framework as zip unsafe.
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index 1e54836c1..efe39d8d4 100755
--- a/setup.py
+++ b/setup.py
@@ -67,6 +67,7 @@ setup(
packages=get_packages('rest_framework'),
package_data=get_package_data('rest_framework'),
install_requires=[],
+ zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
From 58ec7669aed9ebd58fd6095c6a6437bf9f3cf7f1 Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Wed, 7 Jan 2015 18:22:30 +0000
Subject: [PATCH 062/192] 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 063/192] 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 1102e22cb407b9069ce3301bd578ba45a775a89e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 7 Jan 2015 21:02:42 +0000
Subject: [PATCH 064/192] Update project-management.md
---
docs/topics/project-management.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md
index f581cabd3..037b71827 100644
--- a/docs/topics/project-management.md
+++ b/docs/topics/project-management.md
@@ -58,6 +58,8 @@ The following template should be used for the description of the issue, and serv
#### New members.
If you wish to be considered for this or a future date, please comment against this or subsequent issues.
+
+ To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
#### Responsibilities of team members
@@ -107,6 +109,8 @@ The following template should be used for the description of the issue, and serv
- [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework).
- [ ] Make a release announcement on twitter.
- [ ] Close the milestone on GitHub.
+
+ To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages.
From 18cefad3abfa6e6a7bbd278e8d54eef66a1d1e53 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 7 Jan 2015 21:03:50 +0000
Subject: [PATCH 065/192] Update project-management.md
---
docs/topics/project-management.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md
index 037b71827..bcc0330e5 100644
--- a/docs/topics/project-management.md
+++ b/docs/topics/project-management.md
@@ -59,7 +59,7 @@ The following template should be used for the description of the issue, and serv
If you wish to be considered for this or a future date, please comment against this or subsequent issues.
- To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
+ To modify this process for future maintenance cycles make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
#### Responsibilities of team members
From e61ef3d39f301bc62323b47af5080877e273c395 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 11:07:47 +0000
Subject: [PATCH 066/192] Minor docs updates
---
docs/api-guide/filtering.md | 1 +
docs/topics/project-management.md | 1 +
2 files changed, 2 insertions(+)
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 83977048f..3eb1538f1 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -316,6 +316,7 @@ Typically you'd instead control this by setting `order_by` on the initial querys
queryset = User.objects.all()
serializer_class = UserSerializer
filter_backends = (filters.OrderingFilter,)
+ ordering_fields = ('username', 'email')
ordering = ('username',)
The `ordering` attribute may be either a string or a list/tuple of strings.
diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md
index f581cabd3..f052aa83a 100644
--- a/docs/topics/project-management.md
+++ b/docs/topics/project-management.md
@@ -126,6 +126,7 @@ The following issues still need to be addressed:
* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.
* Document ownership of the [live example][sandbox] API.
* Document ownership of the [mailing list][mailing-list] and IRC channel.
+* Document ownership and management of the security mailing list.
[bus-factor]: http://en.wikipedia.org/wiki/Bus_factor
[un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
From f0ad0a88c49f1fef473ef1fbf965bcaa974ee062 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 12:31:51 +0000
Subject: [PATCH 067/192] 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 b33a6cbff16e5a28a1a696e2ac617303da181720 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 14:16:58 +0000
Subject: [PATCH 068/192] Ensure urlparse is not publically exposed in
compat.py - less chance of accidental conflict.
---
rest_framework/compat.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index b1f6f2fa6..971dee9cf 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -10,7 +10,7 @@ import inspect
from django.core.exceptions import ImproperlyConfigured
from django.utils.encoding import force_text
-from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.six.moves.urllib.parse import urlparse as _urlparse
from django.conf import settings
from django.utils import six
import django
@@ -182,7 +182,7 @@ except ImportError:
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
- parsed = urlparse.urlparse(path)
+ parsed = _urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
'PATH_INFO': self._get_path(parsed),
From f529f83d3c96ca1957d7a8bfc74bd33151cc8d86 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 14:38:23 +0000
Subject: [PATCH 069/192] Minimum Django 1.5 version issue 1.5.6
---
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 8fc11c30f..428a2e56b 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.5+, 1.6, 1.7)
+* Django (1.4.11+, 1.5.6+, 1.6, 1.7)
# Installation
diff --git a/docs/index.md b/docs/index.md
index 55129df18..a621e3ecb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -51,7 +51,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.5+, 1.6, 1.7)
+* Django (1.4.11+, 1.5.6+, 1.6, 1.7)
The following packages are optional:
diff --git a/tox.ini b/tox.ini
index 933ee560e..ab258f2ee 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
django14: Django==1.4.11
- django15: Django==1.5.5
+ django15: Django==1.5.6
django16: Django==1.6.8
django17: Django==1.7.1
djangomaster: https://github.com/django/django/zipball/master
From 42c913334b0b4cd731011a07a49ff08aa03d7768 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 14:51:08 +0000
Subject: [PATCH 070/192] Minimum 1.6.x version is 1.6.3
---
README.md | 2 +-
docs/index.md | 2 +-
tox.ini | 8 ++++----
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 428a2e56b..cf3dc8576 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, 1.7)
+* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7)
# Installation
diff --git a/docs/index.md b/docs/index.md
index a621e3ecb..d40f8972f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -51,7 +51,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, 1.7)
+* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7)
The following packages are optional:
diff --git a/tox.ini b/tox.ini
index ab258f2ee..db2b5d13e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,10 +10,10 @@ commands = ./runtests.py --fast
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
- django14: Django==1.4.11
- django15: Django==1.5.6
- django16: Django==1.6.8
- django17: Django==1.7.1
+ django14: Django==1.4.11 # Should track minimum supported
+ django15: Django==1.5.6 # Should track minimum supported
+ django16: Django==1.6.3 # Should track minimum supported
+ django17: Django==1.7.3 # Should track maximum supported
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
From 08008669886f682ba62c4d377f4d96f64808d4a5 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 14:54:54 +0000
Subject: [PATCH 071/192] Fix broken 1.7.3. It's 1.7.2 - 1.7.3 is documented
but not yet on PyPI.
---
tox.ini | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tox.ini b/tox.ini
index db2b5d13e..4b90c3929 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,7 +13,7 @@ deps =
django14: Django==1.4.11 # Should track minimum supported
django15: Django==1.5.6 # Should track minimum supported
django16: Django==1.6.3 # Should track minimum supported
- django17: Django==1.7.3 # Should track maximum supported
+ django17: Django==1.7.2 # Should track maximum supported
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
From 4d9e7a53565f6301b87999e6bafdb1c2c3c2af3b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 8 Jan 2015 15:38:27 +0000
Subject: [PATCH 072/192] Ammend docstring to use python2/3 compatible example.
---
rest_framework/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 33f848138..fc6dfecda 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -167,7 +167,7 @@ class APISettings(object):
For example:
from rest_framework.settings import api_settings
- print api_settings.DEFAULT_RENDERER_CLASSES
+ print(api_settings.DEFAULT_RENDERER_CLASSES)
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
From 1368c31a705a4892995f42cf5e0dcdcbfa13a1ce Mon Sep 17 00:00:00 2001
From: Craig Blaszczyk
Date: Thu, 8 Jan 2015 17:16:15 +0000
Subject: [PATCH 073/192] 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 074/192] 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 ef16c546d77d36bbddacf9b66626f7eaf9f4ff17 Mon Sep 17 00:00:00 2001
From: Xavier Ordoquy
Date: Thu, 8 Jan 2015 23:29:51 +0100
Subject: [PATCH 075/192] Update the release note with latest fixes. Add the
link to the 3.0.3 milestone.
---
docs/topics/release-notes.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 18a47b71b..c49dd62c9 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -59,6 +59,7 @@ You can determine your currently installed version using `pip freeze`:
* Fix Metaclass attribute depth ignoring fields attribute. ([#2287][gh2287])
* Fix `format_suffix_patterns` to work with Django's `i18n_patterns`. ([#2278][gh2278])
* Ability to customize router URLs for custom actions, using `url_path`. ([#2010][gh2010])
+* Don't install Django REST Framework as egg. ([#2386][gh2386])
### 3.0.2
@@ -700,6 +701,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
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
@@ -767,3 +769,4 @@ For older release notes, [please see the GitHub repo](old-release-notes).
[gh2351]: https://github.com/tomchristie/django-rest-framework/issues/2351
[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
From 73feaf6299827607eab94ce96b77b73671880626 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 9 Jan 2015 15:30:36 +0000
Subject: [PATCH 076/192] 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 8ccf5bcc0bb3455c0d71a0e0d845ef54489bb28e Mon Sep 17 00:00:00 2001
From: Travis Swientek
Date: Fri, 9 Jan 2015 11:36:21 -0800
Subject: [PATCH 077/192] Tweaked a few issues in the tutorial documentation.
---
docs/tutorial/1-serialization.md | 2 +-
docs/tutorial/3-class-based-views.md | 2 +-
docs/tutorial/4-authentication-and-permissions.md | 2 +-
docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 ++
4 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 60a3d9897..41ff4d073 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -191,7 +191,7 @@ Our `SnippetSerializer` class is replicating a lot of information that's also co
In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes.
Let's look at refactoring our serializer using the `ModelSerializer` class.
-Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class.
+Open the file `snippets/serializers.py` again, and replace the `SnippetSerializer` class with the following.
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index 0a9ea3f15..abf82e495 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -64,7 +64,7 @@ That's looking good. Again, it's still pretty similar to the function based vie
We'll also need to refactor our `urls.py` slightly now we're using class based views.
- from django.conf.urls import patterns, url
+ from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 592c77e81..887d1e56f 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -177,7 +177,7 @@ In the snippets app, create a new file, `permissions.py`
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
-Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
+Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` view class:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index c21efd7f6..2841f03e9 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -106,6 +106,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this:
+ from django.conf.urls import url, include
+
# API endpoints
urlpatterns = format_suffix_patterns([
url(r'^$', views.api_root),
From 50b206d3739660cdf089b0a3f8a5bb21d6970e00 Mon Sep 17 00:00:00 2001
From: Steven Loria
Date: Sat, 10 Jan 2015 10:17:37 -0600
Subject: [PATCH 078/192] Fix broken links in README
---
README.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index cf3dc8576..74bcaeefa 100644
--- a/README.md
+++ b/README.md
@@ -190,18 +190,18 @@ 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
-[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
-[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html
-[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html
-[routers]: http://www.django-rest-framework.org/api-guide/routers.html
-[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html
-[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#oauthauthentication
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#oauth2authentication
+[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#serializers
+[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#modelserializer
+[functionview-section]: http://www.django-rest-framework.org/api-guide/views/#function-based-views
+[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views/
+[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets/
+[routers]: http://www.django-rest-framework.org/api-guide/routers/
+[serializers]: http://www.django-rest-framework.org/api-guide/serializers/
+[authentication]: http://www.django-rest-framework.org/api-guide/authentication/
-[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html
+[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[image]: http://www.django-rest-framework.org/img/quickstart.png
From d6bff10f9829b3cef1c2773c303b172a8c7ec525 Mon Sep 17 00:00:00 2001
From: Ask Holme
Date: Sat, 10 Jan 2015 18:15:21 +0100
Subject: [PATCH 079/192] Make FileUploadParser work with standard django API
Output from parsers ends up in a Django MergeDict and they exists elements to be dicts - not None
---
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 3e3395c0c..401856ec4 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -277,7 +277,7 @@ class FileUploadParser(BaseParser):
for index, handler in enumerate(upload_handlers):
file_obj = handler.file_complete(counters[index])
if file_obj:
- return DataAndFiles(None, {'file': file_obj})
+ return DataAndFiles({}, {'file': file_obj})
raise ParseError("FileUpload parse error - "
"none of upload handlers can handle the stream")
From d6d08db0dd16f4a4a93b69ecf1c5948f375335b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jose=CC=81=20Padilla?=
Date: Sun, 11 Jan 2015 10:55:56 -0400
Subject: [PATCH 080/192] Fix ident format when using HTTP_X_FORWARDED_FOR
If NUM_PROXIES setting is set to None,
HTTP_X_FORWARDED_FOR might be used as is, which
might contain spaces and cause errors on
cache backends like memcached.
---
rest_framework/throttling.py | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index 361dbddf0..7dfe2f967 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -35,7 +35,7 @@ class BaseThrottle(object):
client_addr = addrs[-min(num_proxies, len(xff))]
return client_addr.strip()
- return xff if xff else remote_addr
+ return ''.join(xff.split()) if xff else remote_addr
def wait(self):
"""
@@ -173,12 +173,6 @@ class AnonRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated():
return None # Only throttle unauthenticated requests.
- ident = request.META.get('HTTP_X_FORWARDED_FOR')
- if ident is None:
- ident = request.META.get('REMOTE_ADDR')
- else:
- ident = ''.join(ident.split())
-
return self.cache_format % {
'scope': self.scope,
'ident': self.get_ident(request)
From cc13ee0577fb3de9602da634ab9c835749da49c4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jos=C3=A9=20Padilla?=
Date: Mon, 12 Jan 2015 08:12:24 -0400
Subject: [PATCH 081/192] Fix error when NUM_PROXIES is greater than one
---
rest_framework/throttling.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index 7dfe2f967..0f10136d6 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -32,7 +32,7 @@ class BaseThrottle(object):
if num_proxies == 0 or xff is None:
return remote_addr
addrs = xff.split(',')
- client_addr = addrs[-min(num_proxies, len(xff))]
+ client_addr = addrs[-min(num_proxies, len(addrs))]
return client_addr.strip()
return ''.join(xff.split()) if xff else remote_addr
From 7f9a62a5bf6a86c4d0a96e5f00d7e96b22d3337f Mon Sep 17 00:00:00 2001
From: Philip Neustrom
Date: Tue, 13 Jan 2015 15:19:52 +0800
Subject: [PATCH 082/192] Fix link to `django-rest-framework-filters` (formerly
`django-rest-framework-chain`)
---
docs/api-guide/filtering.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 07420d842..2b6d54492 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -388,9 +388,9 @@ We could achieve the same behavior by overriding `get_queryset()` on the views,
The following third party packages provide additional filter implementations.
-## Django REST framework chain
+## Django REST framework filters package
-The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
+The [django-rest-framework-filters package][django-rest-framework-filters] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
@@ -400,4 +400,4 @@ The [django-rest-framework-chain package][django-rest-framework-chain] works tog
[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
-[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain
+[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
From 2b28026fc10c2a8d3e4c9ef1f11b2f802a40ec77 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 13 Jan 2015 10:44:40 +0000
Subject: [PATCH 083/192] 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 084/192] 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 085/192] 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 086/192] 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 4ce4132e08ba7764f120c71eeebdbaefee281689 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 12:56:03 +0000
Subject: [PATCH 087/192] Preserve ordering on relationship drop-down choices.
Closes #2408.
---
rest_framework/relations.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 7b119291d..aa0c2defe 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -7,6 +7,7 @@ from django.utils import six
from django.utils.encoding import smart_text
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.utils import html
@@ -103,7 +104,7 @@ class RelatedField(Field):
@property
def choices(self):
- return dict([
+ return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item)
@@ -364,7 +365,7 @@ class ManyRelatedField(Field):
(item, self.child_relation.to_representation(item))
for item in iterable
]
- return dict([
+ return OrderedDict([
(
six.text_type(item_representation),
six.text_type(item) + ' - ' + six.text_type(item_representation)
From 4d287c7aef7b12086930eeb7a05cadb7e8b2cc48 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 13:19:56 +0000
Subject: [PATCH 088/192] 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 089/192] 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 090/192] Include pagination control in browsable API
---
rest_framework/pagination.py | 90 ++++++++++++++++++-
rest_framework/renderers.py | 1 +
.../rest_framework/css/bootstrap-tweaks.css | 4 -
.../templates/rest_framework/base.html | 9 ++
.../rest_framework/pagination/numbers.html | 27 ++++++
rest_framework/templatetags/rest_framework.py | 17 ++++
6 files changed, 143 insertions(+), 5 deletions(-)
create mode 100644 rest_framework/templates/rest_framework/pagination/numbers.html
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index b9d487968..bd343c0dd 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -3,14 +3,18 @@ Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
+from collections import namedtuple
from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.template import Context, loader
from django.utils import six
from django.utils.translation import ugettext as _
from rest_framework.compat import OrderedDict
from rest_framework.exceptions import NotFound
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.templatetags.rest_framework import replace_query_param
+from rest_framework.templatetags.rest_framework import (
+ replace_query_param, remove_query_param
+)
def _strict_positive_int(integer_string, cutoff=None):
@@ -35,6 +39,49 @@ def _get_count(queryset):
return len(queryset)
+def _get_displayed_page_numbers(current, final):
+ """
+ This utility function determines a list of page numbers to display.
+ This gives us a nice contextually relevant set of page numbers.
+
+ For example:
+ current=14, final=16 -> [1, None, 13, 14, 15, 16]
+ """
+ assert current >= 1
+ assert final >= current
+
+ # We always include the first two pages, last two pages, and
+ # two pages either side of the current page.
+ included = set((
+ 1,
+ current - 1, current, current + 1,
+ final
+ ))
+
+ # If the break would only exclude a single page number then we
+ # may as well include the page number instead of the break.
+ if current == 4:
+ included.add(2)
+ if current == final - 3:
+ included.add(final - 1)
+
+ # Now sort the page numbers and drop anything outside the limits.
+ included = [
+ idx for idx in sorted(list(included))
+ if idx > 0 and idx <= final
+ ]
+
+ # Finally insert any `...` breaks
+ if current > 4:
+ included.insert(1, None)
+ if current < final - 3:
+ included.insert(len(included) - 1, None)
+ return included
+
+
+PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
+
+
class BasePagination(object):
def paginate_queryset(self, queryset, request, view):
raise NotImplemented('paginate_queryset() must be implemented.')
@@ -66,6 +113,8 @@ class PageNumberPagination(BasePagination):
# Only relevant if 'paginate_by_param' has also been set.
max_paginate_by = api_settings.MAX_PAGINATE_BY
+ template = 'rest_framework/pagination/numbers.html'
+
def paginate_queryset(self, queryset, request, view):
"""
Paginate a queryset if required, either returning a
@@ -104,6 +153,8 @@ class PageNumberPagination(BasePagination):
)
raise NotFound(msg)
+ # Indicate that the browsable API should display pagination controls.
+ self.mark_as_used = True
self.request = request
return self.page
@@ -139,8 +190,45 @@ class PageNumberPagination(BasePagination):
return None
url = self.request.build_absolute_uri()
page_number = self.page.previous_page_number()
+ if page_number == 1:
+ return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number)
+ def to_html(self):
+ current = self.page.number
+ final = self.page.paginator.num_pages
+
+ page_links = []
+ base_url = self.request.build_absolute_uri()
+ for page_number in _get_displayed_page_numbers(current, final):
+ if page_number is None:
+ page_link = PageLink(
+ url=None,
+ number=None,
+ is_active=False,
+ is_break=True
+ )
+ else:
+ if page_number == 1:
+ url = remove_query_param(base_url, self.page_query_param)
+ else:
+ url = replace_query_param(url, self.page_query_param, page_number)
+ page_link = PageLink(
+ url=url,
+ number=page_number,
+ is_active=(page_number == current),
+ is_break=False
+ )
+ page_links.append(page_link)
+
+ template = loader.get_template(self.template)
+ context = Context({
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ })
+ return template.render(context)
+
class LimitOffsetPagination(BasePagination):
"""
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index c4de30db7..4c002b168 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -592,6 +592,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'description': self.get_description(view),
'name': self.get_name(view),
'version': VERSION,
+ 'pager': getattr(view, 'pager', None),
'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 36c7be481..d4a7d31a2 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -185,10 +185,6 @@ body a:hover {
color: #c20000;
}
-#content a span {
- text-decoration: underline;
- }
-
.request-info {
clear:both;
}
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e96681932..e00309811 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -119,9 +119,18 @@
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 69e03af40..bf159d8b1 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -26,6 +26,23 @@ def replace_query_param(url, key, val):
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+def remove_query_param(url, key):
+ """
+ Given a URL and a key/val pair, set or replace an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = QueryDict(query).copy()
+ query_dict.pop(key, None)
+ query = query_dict.urlencode()
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+
+@register.simple_tag
+def get_pagination_html(pager):
+ return pager.to_html()
+
+
# Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
From 313aa727e3c44016e531a7af75051fc6e6d7cb96 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 14 Jan 2015 17:46:41 +0000
Subject: [PATCH 091/192] Tweaks
---
docs/api-guide/pagination.md | 19 +++++++++++++++----
docs/img/link-header-pagination.png | Bin 0 -> 35799 bytes
rest_framework/pagination.py | 12 ++++++++++--
3 files changed, 25 insertions(+), 6 deletions(-)
create mode 100644 docs/img/link-header-pagination.png
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 9fbeb22a0..ba71a3032 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -74,7 +74,7 @@ Note that the `paginate_queryset` method may set state on the pagination instanc
Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
- class LinkHeaderPagination(PageNumberPagination)
+ class LinkHeaderPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
@@ -82,7 +82,7 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu
link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
elif next_url is not None:
link = '<{next_url}; rel="next">'
- elif prev_url is not None:
+ elif previous_url is not None:
link = '<{previous_url}; rel="prev">'
else:
link = ''
@@ -97,10 +97,20 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
REST_FRAMEWORK = {
- 'DEFAULT_PAGINATION_CLASS':
- 'my_project.apps.core.pagination.LinkHeaderPagination',
+ 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination',
+ 'PAGINATE_BY': 10
}
+API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example:
+
+---
+
+![Link Header][link-header]
+
+*A custom pagination style, using the 'Link' header'*
+
+---
+
# Third party packages
The following third party packages are also available.
@@ -111,5 +121,6 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
+[link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
diff --git a/docs/img/link-header-pagination.png b/docs/img/link-header-pagination.png
new file mode 100644
index 0000000000000000000000000000000000000000..d3c556a4d6aebc3dc1575c43eced4059d864bab2
GIT binary patch
literal 35799
zcmZTv19W9c*NvU-*y-4|)v?*JZQEwYb|>lBwr$(#*f#!j&wMkP^=Ga3)_rxWPF0;-
zr}p0GRzhT?gkhmDpa1{>U`0g)$whv9&TZ
zF*g7J5P>aCa8f{2!R$MeaLob5$0fRsum=(;2PB)~Q}~JiLDk1=?5J-8s)&RPFQaIG
z;9D$b5e&MVy--0jfd8W;43x6q7=(K(?sd0yyv1%g?FxXmOdk@2tO5q0
z@Y!Ccq8^iro;)Su37Z!H7=jmFM){c0$4q8|{=D4akvm&x2piF+%q#xo3{d40OE@e5
zl0Qtp&D266M5cf#wHmGjI6yD&)>85T5M4hmC5-@aa0TxOa;&tGeHV5
zz_~t(coG0WA#lhv!vdtpmnmTX(fc$3ZUBAGE-CXbVSW{#yZQ(Uc#to
ze@P_cu5N6=U8QpNL7Yy`f*wJ9@TaOEWgBtuiJiX}y?$=+?g(Jz7{y+EdPrkynFt2m
zwdUePRc;_w&XyjAi0-j-J_UqMAxtR0Z`s@JL%_er+0AnnZ=(?T95WJ|iGZsj-ZCMc
zy_x5sWyMaT;O=S1HXV?KAnBfsYKJ$5_$adF8`sFem5&Y)qNRtOce}(
z(t)6ihy2s7y5|nKp&VWx8WGX?z+P#4*Dm}(f_r%;$x_GA61L|pS{b;b8Qks-T%g-4
z(jXJOV+YAr7`d$6_n4V^G_4*tj_Qg|RJ<6l%MXwlZy$x4f+pg17Xc{^
zs{0xs1F7%{+xIX_D@$sh(;oRS=*w6B3D#?rN0=OKddknmT}|f*Nr25=p4Ox-2vXkj
zU2N80T7aqjiZ_WHV7WhQ1)X*Jol`hNfyxjf!RGqV^U=wi7Vs)Cl7Ar*un<80AtpOp
zV5opvgz5m!6r95A@l|h9ff3un*OB+>NB4yJwd%EyJ$w_EMvx`HmcTmS=PZ)Rby}qF
zP<;@3kvh8gbdX6B65M%6@_r`0>Rl_{-?j59l__D1gKB#pwR0*&7aSJwj{(i0>oAui
zvHPSptyziE;K$%mgY`D2Y#th38i-feRxDN^P6;ynmHO2-TF)n)xw%obLajP7d)NCS
zw~Qd@L9l(f@S-~K^btcLg&{QjQ~lw)U}O^tgzX6f5n_YYf(?2Mw}`e7w>S_&eu`ER
zP>~SDFvkvw^ouBo@c)$7XRE?nOvmC!iFS%I8(iHE+IB*M5=`9s8j|-@-cja|43%`8
z6eqVpE>BK^EMkVZ5UP;5a9OTA7jlYys%)zGr`=ESTxpT@xQr;gf&HO|;R=1`A%y|^
zA(#P)K{eSr$kX78SY{Ln+)9Ys#d{