From 4c8bd40465a79679b0db26277b59448db63d09d0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 16:42:24 +0000 Subject: [PATCH 001/128] Tests for DjangoModelPermissions. --- rest_framework/tests/permissions.py | 89 +++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 rest_framework/tests/permissions.py diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py new file mode 100644 index 000000000..c04d21102 --- /dev/null +++ b/rest_framework/tests/permissions.py @@ -0,0 +1,89 @@ +from __future__ import unicode_literals +from django.contrib.auth.models import User, Permission +from django.db import models +from django.test import TestCase +from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING +from rest_framework.tests.utils import RequestFactory +import base64 +import json + +factory = RequestFactory() + + +class BasicModel(models.Model): + text = models.CharField(max_length=100) + + +class RootView(generics.ListCreateAPIView): + model = BasicModel + authentication_classes = [authentication.BasicAuthentication] + permission_classes = [permissions.DjangoModelPermissions] + + +class InstanceView(generics.RetrieveUpdateDestroyAPIView): + model = BasicModel + authentication_classes = [authentication.BasicAuthentication] + permission_classes = [permissions.DjangoModelPermissions] + +root_view = RootView.as_view() +instance_view = InstanceView.as_view() + + +def basic_auth_header(username, password): + credentials = ('%s:%s' % (username, password)) + base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING) + return 'Basic %s' % base64_credentials + + +class ModelPermissionsIntegrationTests(TestCase): + def setUp(self): + User.objects.create_user('disallowed', 'disallowed@example.com', 'password') + user = User.objects.create_user('permitted', 'permitted@example.com', 'password') + user.user_permissions = [ + Permission.objects.get(codename='add_basicmodel'), + Permission.objects.get(codename='change_basicmodel'), + Permission.objects.get(codename='delete_basicmodel') + ] + + self.permitted_credentials = basic_auth_header('permitted', 'password') + self.disallowed_credentials = basic_auth_header('disallowed', 'password') + + BasicModel(text='foo').save() + + def test_has_create_permissions(self): + request = factory.post('/', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.permitted_credentials) + response = root_view(request, pk=1) + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + + def test_has_put_permissions(self): + request = factory.put('/1', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.permitted_credentials) + response = instance_view(request, pk='1') + self.assertEquals(response.status_code, status.HTTP_200_OK) + + def test_has_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials) + response = instance_view(request, pk=1) + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_does_not_have_create_permissions(self): + request = factory.post('/', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.disallowed_credentials) + response = root_view(request, pk=1) + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_does_not_have_put_permissions(self): + request = factory.put('/1', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.disallowed_credentials) + response = instance_view(request, pk='1') + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_does_not_have_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials) + response = instance_view(request, pk=1) + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) From 69dcf13da90e4a2c78ea4136426fa98d24a83813 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 16:43:52 +0000 Subject: [PATCH 002/128] Bugfix for DjangoModelPermissions. Fixes #437 Turns out that Django's default permissions backend always returns False when checking object-level permissions, even if the user does have valid global permissions. --- rest_framework/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 3222dbf22..c9bbf4c4f 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -109,6 +109,6 @@ class DjangoModelPermissions(BasePermission): if (request.user and request.user.is_authenticated() and - request.user.has_perms(perms, obj)): + request.user.has_perms(perms)): return True return False From 0997ce9fc20cd9ff8e63246c3892d28b411bf857 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 16:44:32 +0000 Subject: [PATCH 003/128] Improve relations documentation. --- docs/api-guide/relations.md | 93 +++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 14 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index c5da084b5..25fca4753 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -32,6 +32,7 @@ In order to explain the various types of relational fields, we'll use a couple o class Meta: unique_together = ('album', 'order') + order_by = 'order' def __unicode__(self): return '%d: %s' % (self.order, self.title) @@ -64,6 +65,10 @@ Would serialize to the following representation. This field is read only. +**Arguments**: + +* `many` - If applied to a to-many relationship, you should set this argument to `True`. + ## PrimaryKeyRelatedField `PrimaryKeyRelatedField` may be used to represent the target of the relationship using it's primary key. @@ -94,8 +99,9 @@ By default this field is read-write, although you can change this behavior using **Arguments**: -* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `many` - If applied to a to-many relationship, you should set this argument to `True`. * `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships. +* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. ## HyperlinkedRelatedField @@ -129,6 +135,7 @@ By default this field is read-write, although you can change this behavior using **Arguments**: * `view_name` - The view name that should be used as the target of the relationship. **required**. +* `many` - If applied to a to-many relationship, you should set this argument to `True`. * `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. * `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. @@ -168,16 +175,17 @@ When using `SlugRelatedField` as a read-write field, you will normally want to e **Arguments**: -* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. +* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. **required** +* `many` - If applied to a to-many relationship, you should set this argument to `True`. +* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. -* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships. -## HyperLinkedIdentityField +## HyperlinkedIdentityField This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer: class AlbumSerializer(serializers.HyperlinkedModelSerializer): - track_listing = HyperLinkedIdentityField(view_name='track-list') + track_listing = HyperlinkedIdentityField(view_name='track-list') class Meta: model = Album @@ -201,12 +209,23 @@ This field is always read-only. * `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. * `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. -## Nested relationships +--- -Nested relationships can be expressed by using serializers as fields. For example: +# Nested relationships + +Nested relationships can be expressed by using serializers as fields. + +If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. + +Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style. + +## Example + +For example, the following serializer: class TrackSerializer(serializer.ModelSerializer): class Meta: + model = Track fields = ('order', 'title') class AlbumSerializer(serializer.ModelSerializer): @@ -216,17 +235,57 @@ Nested relationships can be expressed by using serializers as fields. For examp model = Album fields = ('album_name', 'artist', 'tracks') -Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style. +Would serialize to a nested representation like this: -## Custom relational fields + { + 'album_name': 'The Grey Album', + 'artist': 'Danger Mouse' + 'tracks': [ + {'order': 1, 'title': 'Public Service Annoucement'}, + {'order': 2, 'title': 'What More Can I Say'}, + {'order': 3, 'title': 'Encore'}, + ... + ], + } + +# Custom relational fields To implement a custom relational field, you should override `RelatedField`, and implement the `.to_native(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target. +If you want to implement a read-write relational field, you must also implement the `.from_native(self, data)` method, and add `read_only = False` to the class definition. + +## Example + +For, example, we could define a relational field, to serialize a track to a custom string representation, using it's ordering, title, and duration. + + import time + class TrackListingField(serializers.RelatedField): def to_native(self, value): - return 'Track %d: %s' % (value.ordering, value.name) + duration = time.strftime('%M:%S', time.gmtime(value.duration)) + return 'Track %d: %s (%s)' % (value.order, value.name, duration) -If you want to implement a read-write relational field, you must also implement the `.from_native(self, data)` method, and add `read_only = False` to the class definition. + class AlbumSerializer(serializer.ModelSerializer): + tracks = TrackListingField(many=True) + + class Meta: + model = Album + fields = ('album_name', 'artist', 'tracks') + +This custom field would then serialize to the following representation. + + { + 'album_name': 'Sometimes I Wish We Were an Eagle', + 'artist': 'Bill Callahan' + 'tracks': [ + 'Track 1: Jim Cain (04:39)', + 'Track 2: Eid Ma Clack Shaw (04:19)', + 'Track 3: The Wind and the Dove (04:34)', + ... + ] + } + +--- # Further notes @@ -337,18 +396,24 @@ For more information see [the Django documentation on generic relations][generic --- -## Deprecated relational fields +## Deprecated APIs The following classes have been deprecated, in favor of the `many=` syntax. They continue to function, but their usage will raise a `PendingDeprecationWarning`, which is silent by default. -In the 2.3 release, this warning will be escalated to a `DeprecationWarning`. -In the 2.4 release, they will be removed entirely. * `ManyRelatedField` * `ManyPrimaryKeyRelatedField` * `ManyHyperlinkedRelatedField` * `ManySlugRelatedField` +The `null=` flag has been deprecated in favor of the `required=` flag. It will continue to function, but will raise a `PendingDeprecationWarning`. + +In the 2.3 release, these warnings will be escalated to a `DeprecationWarning`, which is loud by default. +In the 2.4 release, these parts of the API will be removed entirely. + +For more details see the [2.2 release announcement][2.2-announcement]. + [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 +[2.2-announcement]: ../topics/2.2-announcement.md From 84a1896b7de5c2e3fc5f564027e5fccd7b2447f9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 16:44:45 +0000 Subject: [PATCH 004/128] Change URL of 2.2 announcement --- docs/topics/{2.2-release-notes.md => 2.2-announcement.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/topics/{2.2-release-notes.md => 2.2-announcement.md} (100%) diff --git a/docs/topics/2.2-release-notes.md b/docs/topics/2.2-announcement.md similarity index 100% rename from docs/topics/2.2-release-notes.md rename to docs/topics/2.2-announcement.md From 29136ef2c6338b8dbc9f7cf9c4dd75867a6bfa9f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 16:50:46 +0000 Subject: [PATCH 005/128] Enforce PUT-as-create permissions --- rest_framework/mixins.py | 5 +++++ rest_framework/tests/permissions.py | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 61ac225ba..ce6331127 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -9,6 +9,7 @@ from __future__ import unicode_literals from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from rest_framework.request import clone_request class CreateModelMixin(object): @@ -90,6 +91,10 @@ class UpdateModelMixin(object): try: self.object = self.get_object() except Http404: + # If this is a PUT-as-create operation, we need to ensure that + # we have relevant permissions, as if this was a POST request. + if not self.has_permission(clone_request(request, 'POST')): + self.permission_denied(self.request) created = True success_status_code = status.HTTP_201_CREATED else: diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py index c04d21102..a7777b577 100644 --- a/rest_framework/tests/permissions.py +++ b/rest_framework/tests/permissions.py @@ -44,9 +44,14 @@ class ModelPermissionsIntegrationTests(TestCase): Permission.objects.get(codename='change_basicmodel'), Permission.objects.get(codename='delete_basicmodel') ] + user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password') + user.user_permissions = [ + Permission.objects.get(codename='change_basicmodel'), + ] self.permitted_credentials = basic_auth_header('permitted', 'password') self.disallowed_credentials = basic_auth_header('disallowed', 'password') + self.updateonly_credentials = basic_auth_header('updateonly', 'password') BasicModel(text='foo').save() @@ -87,3 +92,18 @@ class ModelPermissionsIntegrationTests(TestCase): request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials) response = instance_view(request, pk=1) self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_has_put_as_create_permissions(self): + # User only has update permissions - should be able to update an entity. + request = factory.put('/1', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.updateonly_credentials) + response = instance_view(request, pk='1') + self.assertEquals(response.status_code, status.HTTP_200_OK) + + # But if PUTing to a new entity, permission should be denied. + request = factory.put('/2', json.dumps({'text': 'foobar'}), + content_type='application/json', + HTTP_AUTHORIZATION=self.updateonly_credentials) + response = instance_view(request, pk='2') + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) From 870f10486cd347480fb16d95647d1ca4a72d83d4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 20:08:36 +0000 Subject: [PATCH 006/128] Fix incorrect 401 vs 403 response, if lazy authentication has not taken place. --- rest_framework/request.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 482c86888..47c009b2b 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -88,7 +88,6 @@ class Request(object): self._method = Empty self._content_type = Empty self._stream = Empty - self._authenticator = None if self.parser_context is None: self.parser_context = {} @@ -175,12 +174,12 @@ class Request(object): @user.setter def user(self, value): - """ - Sets the user on the current request. This is necessary to maintain - compatilbility with django.contrib.auth where the user proprety is - set in the login and logout functions. - """ - self._user = value + """ + Sets the user on the current request. This is necessary to maintain + compatilbility with django.contrib.auth where the user proprety is + set in the login and logout functions. + """ + self._user = value @property def auth(self): @@ -206,6 +205,8 @@ class Request(object): Return the instance of the authentication instance class that was used to authenticate the request, or `None`. """ + if not hasattr(self, '_authenticator'): + self._authenticator, self._user, self._auth = self._authenticate() return self._authenticator def _load_data_and_files(self): From baacdd821feece9c77ad74c25fd00842f47cfb84 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 10 Feb 2013 20:08:46 +0000 Subject: [PATCH 007/128] Add object permissions tests. --- rest_framework/tests/permissions.py | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py index a7777b577..26a343193 100644 --- a/rest_framework/tests/permissions.py +++ b/rest_framework/tests/permissions.py @@ -107,3 +107,49 @@ class ModelPermissionsIntegrationTests(TestCase): HTTP_AUTHORIZATION=self.updateonly_credentials) response = instance_view(request, pk='2') self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + + +class OwnerModel(models.Model): + text = models.CharField(max_length=100) + owner = models.ForeignKey(User) + + +class IsOwnerPermission(permissions.BasePermission): + def has_permission(self, request, view, obj=None): + if not obj: + return True + return request.user == obj.owner + + +class OwnerInstanceView(generics.RetrieveUpdateDestroyAPIView): + model = OwnerModel + authentication_classes = [authentication.BasicAuthentication] + permission_classes = [IsOwnerPermission] + + +owner_instance_view = OwnerInstanceView.as_view() + + +class ObjectPermissionsIntegrationTests(TestCase): + """ + Integration tests for the object level permissions API. + """ + + def setUp(self): + User.objects.create_user('not_owner', 'not_owner@example.com', 'password') + user = User.objects.create_user('owner', 'owner@example.com', 'password') + + self.not_owner_credentials = basic_auth_header('not_owner', 'password') + self.owner_credentials = basic_auth_header('owner', 'password') + + OwnerModel(text='foo', owner=user).save() + + def test_owner_has_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.owner_credentials) + response = owner_instance_view(request, pk='1') + self.assertEquals(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_non_owner_does_not_have_delete_permissions(self): + request = factory.delete('/1', HTTP_AUTHORIZATION=self.not_owner_credentials) + response = owner_instance_view(request, pk='1') + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) From ea004b5e7a51ccf176545642692462dc2086056d Mon Sep 17 00:00:00 2001 From: Fernando Rocha Date: Mon, 11 Feb 2013 19:18:22 -0300 Subject: [PATCH 008/128] Make use o issubclass instead of isinstance (fix issue #645) Because __mro__ is a list of classes and not instances. DictWithMetadata.__getstate__ was never called Signed-off-by: Fernando Rocha --- rest_framework/serializers.py | 2 +- rest_framework/tests/serializer.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4fb802a7c..df1e9fae9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -31,7 +31,7 @@ class DictWithMetadata(dict): """ # return an instance of the first dict in MRO that isn't a DictWithMetadata for base in self.__class__.__mro__: - if not isinstance(base, DictWithMetadata) and isinstance(base, dict): + if not issubclass(base, DictWithMetadata) and issubclass(base, dict): return base(self) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 48b4f1ab9..4f4508f5b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -870,6 +870,13 @@ class SerializerPickleTests(TestCase): fields = ('name', 'age') pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data) + def test_getstate_method_should_not_return_none(self): + '''Regression test for + https://github.com/tomchristie/django-rest-framework/issues/645 + ''' + d = serializers.DictWithMetadata({1:1}) + self.assertEqual(d.__getstate__(), serializers.SortedDict({1:1})) + class DepthTest(TestCase): def test_implicit_nesting(self): From 09b01887f234be55c14943028330f569823b2369 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 11 Feb 2013 12:47:56 +0000 Subject: [PATCH 009/128] New style object-level permission checks --- docs/api-guide/permissions.md | 39 +++++++++++++++++-- .../4-authentication-and-permissions.md | 6 +-- rest_framework/generics.py | 2 +- rest_framework/permissions.py | 27 +++++++++---- rest_framework/renderers.py | 2 +- rest_framework/tests/permissions.py | 4 +- rest_framework/tests/renderers.py | 2 +- rest_framework/views.py | 18 ++++++++- 8 files changed, 78 insertions(+), 22 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 1814b8110..5cb3ec3c0 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -106,22 +106,55 @@ The `DjangoModelPermissions` class also supports object-level permissions. Thir # Custom permissions -To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, view, obj=None)` method. +To implement a custom permission, override `BasePermission` and implement either, or both, of the `.has_permission(self, request, view)` and `.has_object_permission(self, request, view, obj)` methods. -The method should return `True` if the request should be granted access, and `False` otherwise. +The methods should return `True` if the request should be granted access, and `False` otherwise. -## Example +--- + +**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required. + +As of version 2.2 this signature has now been replaced with two seperate method calls, which is more explict, and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed. + +For more details see the [2.2 release announcement][2.2-announcement]. + +--- + +## Examples The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted. class BlacklistPermission(permissions.BasePermission): + """ + Global permission check for blacklisted IPs. + """ + def has_permission(self, request, view, obj=None): ip_addr = request.META['REMOTE_ADDR'] blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists() return not blacklisted +As well as global permissions, that are run against all incoming requests, you can also create object-level permissions, that are only run against operations that affect a particular object instance. For example: + + class IsOwnerOrReadOnly(permissions.BasePermission): + """ + Object-level permission to only allow owners of an object to edit it. + """ + + def has_object_permission(self, request, view, obj): + # Read permissions are allowed to any request, + # so we'll always allow GET, HEAD or OPTIONS requests. + if request.method in permissions.SAFE_METHODS: + return True + + # Instance must have an attribute named `owner`. + return obj.owner == request.user + +Note that the generic views will check the appropriate object level permissions, but if you're writing your own custom views, you'll need to make sure you check the object level permission checks yourself, by calling `self.has_object_permission(request, obj)` from the view. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions [guardian]: https://github.com/lukaszb/django-guardian +[2.2-announcement]: ../topics/2.2-announcement.md diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index e9e5246a6..979421ea6 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -161,11 +161,7 @@ In the snippets app, create a new file, `permissions.py` Custom permission to only allow owners of an object to edit it. """ - def has_permission(self, request, view, obj=None): - # Skip the check unless this is an object-level test - if obj is None: - return True - + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request, # so we'll always allow GET, HEAD or OPTIONS requests. if request.method in permissions.SAFE_METHODS: diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 5abb915ba..19dca7e6c 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -131,7 +131,7 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): Override default to add support for object-level permissions. """ obj = super(SingleObjectAPIView, self).get_object(queryset) - if not self.has_permission(self.request, obj): + if not self.has_object_permission(self.request, obj): self.permission_denied(self.request) return obj diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index c9bbf4c4f..306f00ca2 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -2,6 +2,8 @@ Provides a set of pluggable permission policies. """ from __future__ import unicode_literals +import inspect +import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] @@ -11,11 +13,22 @@ class BasePermission(object): A base class from which all permission classes should inherit. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): """ Return `True` if permission is granted, `False` otherwise. """ - raise NotImplementedError(".has_permission() must be overridden.") + return True + + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + if len(inspect.getargspec(self.has_permission)[0]) == 4: + warnings.warn('The `obj` argument in `has_permission` is due to be deprecated. ' + 'Use `has_object_permission()` instead for object permissions.', + PendingDeprecationWarning, stacklevel=2) + return self.has_permission(request, view, obj) + return True class AllowAny(BasePermission): @@ -25,7 +38,7 @@ class AllowAny(BasePermission): permission_classes list, but it's useful because it makes the intention more explicit. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): return True @@ -34,7 +47,7 @@ class IsAuthenticated(BasePermission): Allows access only to authenticated users. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): if request.user and request.user.is_authenticated(): return True return False @@ -45,7 +58,7 @@ class IsAdminUser(BasePermission): Allows access only to admin users. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): if request.user and request.user.is_staff: return True return False @@ -56,7 +69,7 @@ class IsAuthenticatedOrReadOnly(BasePermission): The request is authenticated as a user, or is a read-only request. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): if (request.method in SAFE_METHODS or request.user and request.user.is_authenticated()): @@ -100,7 +113,7 @@ class DjangoModelPermissions(BasePermission): } return [perm % kwargs for perm in self.perms_map[method]] - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): model_cls = getattr(view, 'model', None) if not model_cls: return True diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 960d48497..e7df87583 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -301,7 +301,7 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request, obj): + if not view.has_permission(request): return # Don't have permission except Exception: return # Don't have permission and exception explicitly raise diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py index 26a343193..b8e1d89c8 100644 --- a/rest_framework/tests/permissions.py +++ b/rest_framework/tests/permissions.py @@ -115,9 +115,7 @@ class OwnerModel(models.Model): class IsOwnerPermission(permissions.BasePermission): - def has_permission(self, request, view, obj=None): - if not obj: - return True + def has_object_permission(self, request, view, obj): return request.user == obj.owner diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 724053360..e3f45ce60 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -95,7 +95,7 @@ urlpatterns = patterns('', class POSTDeniedPermission(permissions.BasePermission): - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): return request.method != 'POST' diff --git a/rest_framework/views.py b/rest_framework/views.py index fd6b4313f..dd8889aeb 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -13,6 +13,7 @@ from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings import re +import warnings def _remove_trailing_string(content, trailing): @@ -261,8 +262,23 @@ class APIView(View): """ Return `True` if the request should be permitted. """ + if obj is not None: + warnings.warn('The `obj` argument in `has_permission` is due to be deprecated. ' + 'Use `has_object_permission()` instead for object permissions.', + PendingDeprecationWarning, stacklevel=2) + return self.has_object_permission(request, obj) + for permission in self.get_permissions(): - if not permission.has_permission(request, self, obj): + if not permission.has_permission(request, self): + return False + return True + + def has_object_permission(self, request, obj): + """ + Return `True` if the request should be permitted for a given object. + """ + for permission in self.get_permissions(): + if not permission.has_object_permission(request, self, obj): return False return True From f5a0275547ad264c8a9b9aa2a45cc461723a4f11 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 11 Feb 2013 13:02:20 +0000 Subject: [PATCH 010/128] Tidy up internal view permission checking logic. Also document correctly - these methods are now public and will fall under the deprecation policy from now on. --- docs/api-guide/permissions.md | 2 +- docs/api-guide/views.md | 6 +++--- rest_framework/generics.py | 3 +-- rest_framework/mixins.py | 3 +-- rest_framework/renderers.py | 11 ++++------- rest_framework/views.py | 26 ++++++++++---------------- 6 files changed, 20 insertions(+), 31 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 5cb3ec3c0..4845ac88e 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -150,7 +150,7 @@ As well as global permissions, that are run against all incoming requests, you c # Instance must have an attribute named `owner`. return obj.owner == request.user -Note that the generic views will check the appropriate object level permissions, but if you're writing your own custom views, you'll need to make sure you check the object level permission checks yourself, by calling `self.has_object_permission(request, obj)` from the view. +Note that the generic views will check the appropriate object level permissions, but if you're writing your own custom views, you'll need to make sure you check the object level permission checks yourself. You can do so by calling `self.check_object_permissions(request, obj)` from the view once you have the object instance. This call will raise an appropriate `APIException` if any object-level permission checks fail, and will otherwise simply return. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 574020f9b..8b26b3e35 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -76,11 +76,11 @@ The following methods are used by REST framework to instantiate the various plug The following methods are called before dispatching to the handler method. -### .check_permissions(...) +### .check_permissions(self, request) -### .check_throttles(...) +### .check_throttles(self, request) -### .perform_content_negotiation(...) +### .perform_content_negotiation(self, request, force=False) ## Dispatch methods diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 19dca7e6c..9ae8cf0aa 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -131,8 +131,7 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): Override default to add support for object-level permissions. """ obj = super(SingleObjectAPIView, self).get_object(queryset) - if not self.has_object_permission(self.request, obj): - self.permission_denied(self.request) + self.check_object_permissions(self.request, obj) return obj diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index ce6331127..d898ca128 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -93,8 +93,7 @@ class UpdateModelMixin(object): except Http404: # If this is a PUT-as-create operation, we need to ensure that # we have relevant permissions, as if this was a POST request. - if not self.has_permission(clone_request(request, 'POST')): - self.permission_denied(self.request) + self.check_permissions(clone_request(request, 'POST')) created = True success_status_code = status.HTTP_201_CREATED else: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e7df87583..a65254042 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -21,8 +21,7 @@ from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework import VERSION, status -from rest_framework import parsers +from rest_framework import exceptions, parsers, status, VERSION class BaseRenderer(object): @@ -299,12 +298,10 @@ class BrowsableAPIRenderer(BaseRenderer): if not api_settings.FORM_METHOD_OVERRIDE: return # Cannot use form overloading - request = clone_request(request, method) try: - if not view.has_permission(request): - return # Don't have permission - except Exception: - return # Don't have permission and exception explicitly raise + view.check_permissions(clone_request(request, method)) + except exceptions.APIException: + return False # Doesn't have permissions return True def serializer_to_form_fields(self, serializer): diff --git a/rest_framework/views.py b/rest_framework/views.py index dd8889aeb..55ad8cf35 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -258,33 +258,28 @@ class APIView(View): return (renderers[0], renderers[0].media_type) raise - def has_permission(self, request, obj=None): + def check_permissions(self, request): """ - Return `True` if the request should be permitted. + Check if the request should be permitted. + Raises an appropriate exception if the request is not permitted. """ - if obj is not None: - warnings.warn('The `obj` argument in `has_permission` is due to be deprecated. ' - 'Use `has_object_permission()` instead for object permissions.', - PendingDeprecationWarning, stacklevel=2) - return self.has_object_permission(request, obj) - for permission in self.get_permissions(): if not permission.has_permission(request, self): - return False - return True + self.permission_denied(request) - def has_object_permission(self, request, obj): + def check_object_permissions(self, request, obj): """ - Return `True` if the request should be permitted for a given object. + Check if the request should be permitted for a given object. + Raises an appropriate exception if the request is not permitted. """ for permission in self.get_permissions(): if not permission.has_object_permission(request, self, obj): - return False - return True + self.permission_denied(request) def check_throttles(self, request): """ Check if request should be throttled. + Raises an appropriate exception if the request is throttled. """ for throttle in self.get_throttles(): if not throttle.allow_request(request, self): @@ -311,8 +306,7 @@ class APIView(View): self.format_kwarg = self.get_format_suffix(**kwargs) # Ensure that the incoming request is permitted - if not self.has_permission(request): - self.permission_denied(request) + self.check_permissions(request) self.check_throttles(request) # Perform content negotiation and store the accepted info on the request From 55fdac4176ac6629481a8cca8dd2463da9c594a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 08:57:23 +0000 Subject: [PATCH 011/128] Use `many=True` for serializers. --- docs/tutorial/1-serialization.md | 29 ++++++++++++------- docs/tutorial/2-requests-and-responses.md | 2 +- docs/tutorial/3-class-based-views.md | 2 +- .../4-authentication-and-permissions.md | 2 +- .../5-relationships-and-hyperlinked-apis.md | 6 ++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 5f292211d..af5e83134 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -8,7 +8,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o --- -**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. As pieces of code are introduced, they are committed to this repository. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. +**Note**: The code for this tutorial is available in the [tomchristie/rest-framework-tutorial][repo] repository on GitHub. The completed implementation is also online as a sandbox version for testing, [available here][sandbox]. --- @@ -150,13 +150,16 @@ Before we go any further we'll familiarize ourselves with using our new Serializ python manage.py shell -Okay, once we've got a few imports out of the way, let's create a code snippet to work with. +Okay, once we've got a few imports out of the way, let's create a couple of code snippets to work with. from snippets.models import Snippet from snippets.serializers import SnippetSerializer from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser + snippet = Snippet(code='foo = "bar"\n') + snippet.save() + snippet = Snippet(code='print "hello, world"\n') snippet.save() @@ -164,13 +167,13 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} At this point we've translated the model instance into python native datatypes. To finalize the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content - # '{"pk": 1, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' + # '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into python native datatypes... @@ -189,6 +192,12 @@ Deserialization is similar. First we parse a stream into python native datatype Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer. +We can also serialize querysets instead of model instances. To do so we simply add a `many=True` flag to the serializer arguments. + + serializer = SnippetSerializer(Snippet.objects.all(), many=True) + serializer.data + # [{'pk': 1, 'title': u'', 'code': u'foo = "bar"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}, {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}] + ## Using ModelSerializers Our `SnippetSerializer` class is replicating a lot of information that's also contained in the `Snippet` model. It would be nice if we could keep out code a bit more concise. @@ -237,7 +246,7 @@ The root of our API is going to be a view that supports listing all the existing """ if request.method == 'GET': snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets) + serializer = SnippetSerializer(snippets, many=True) return JSONResponse(serializer.data) elif request.method == 'POST': @@ -312,19 +321,19 @@ and start up Django's development server In another terminal window, we can test the server. -We can get a list of all of the snippets (we only have one at the moment) +We can get a list of all of the snippets. curl http://127.0.0.1:8000/snippets/ - [{"id": 1, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}] + [{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}] or we can get a particular snippet by referencing its id - curl http://127.0.0.1:8000/snippets/1/ + curl http://127.0.0.1:8000/snippets/2/ - {"id": 1, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"} + {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"} -Similarly, you can have the same json displayed by referencing these URLs from your favorite web browser. +Similarly, you can have the same json displayed by visiting these URLs in a web browser. ## Where are we now diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 340ea28ee..566c0dc66 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -51,7 +51,7 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On """ if request.method == 'GET': snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets) + serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) elif request.method == 'POST': diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 290ea5e9d..e05017c50 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -20,7 +20,7 @@ We'll start by rewriting the root view as a class based view. All this involves """ def get(self, request, format=None): snippets = Snippet.objects.all() - serializer = SnippetSerializer(snippets) + serializer = SnippetSerializer(snippets, many=True) return Response(serializer.data) def post(self, request, format=None): diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 979421ea6..282386c79 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -57,7 +57,7 @@ Now that we've got some users to work with, we'd better add representations of t from django.contrib.auth.models import User class UserSerializer(serializers.ModelSerializer): - snippets = serializers.ManyPrimaryKeyRelatedField() + snippets = serializers.PrimaryKeyRelatedField(many=True) class Meta: model = User diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index de8566112..81be333bc 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -70,8 +70,8 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial * It does not include the `pk` field by default. * It includes a `url` field, using `HyperlinkedIdentityField`. -* Relationships use `HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField`, - instead of `PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField`. +* Relationships use `HyperlinkedRelatedField`, + instead of `PrimaryKeyRelatedField`. We can easily re-write our existing serializers to use hyperlinking. @@ -86,7 +86,7 @@ We can easily re-write our existing serializers to use hyperlinking. class UserSerializer(serializers.HyperlinkedModelSerializer): - snippets = serializers.ManyHyperlinkedRelatedField(view_name='snippet-detail') + snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail') class Meta: model = User From c81b2c64425353a6a872f2d6f94fd9a9ae063b9a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 08:57:45 +0000 Subject: [PATCH 012/128] Notes on object-level permissions. --- docs/api-guide/permissions.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 4845ac88e..d47dbc35e 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -106,15 +106,25 @@ The `DjangoModelPermissions` class also supports object-level permissions. Thir # Custom permissions -To implement a custom permission, override `BasePermission` and implement either, or both, of the `.has_permission(self, request, view)` and `.has_object_permission(self, request, view, obj)` methods. +To implement a custom permission, override `BasePermission` and implement either, or both, of the following methods: + +* `.has_permission(self, request, view)` +* `.has_object_permission(self, request, view, obj)` The methods should return `True` if the request should be granted access, and `False` otherwise. +If you need to test if a request is a read operation or a write operation, you should check the request method against the constant `SAFE_METHODS`, which is a tuple containing `'GET'`, `'OPTIONS'` and `'HEAD'`. For example: + + if request.method in permissions.SAFE_METHODS: + # Check permissions for read-only request + else: + # Check permissions for write request + --- **Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required. -As of version 2.2 this signature has now been replaced with two seperate method calls, which is more explict, and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed. +As of version 2.2 this signature has now been replaced with two seperate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed. For more details see the [2.2 release announcement][2.2-announcement]. @@ -129,7 +139,7 @@ The following is an example of a permission class that checks the incoming reque Global permission check for blacklisted IPs. """ - def has_permission(self, request, view, obj=None): + def has_permission(self, request, view): ip_addr = request.META['REMOTE_ADDR'] blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists() return not blacklisted @@ -139,6 +149,7 @@ As well as global permissions, that are run against all incoming requests, you c class IsOwnerOrReadOnly(permissions.BasePermission): """ Object-level permission to only allow owners of an object to edit it. + Assumes the model instance has an `owner` attribute. """ def has_object_permission(self, request, view, obj): @@ -152,9 +163,12 @@ As well as global permissions, that are run against all incoming requests, you c Note that the generic views will check the appropriate object level permissions, but if you're writing your own custom views, you'll need to make sure you check the object level permission checks yourself. You can do so by calling `self.check_object_permissions(request, obj)` from the view once you have the object instance. This call will raise an appropriate `APIException` if any object-level permission checks fail, and will otherwise simply return. +Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions [guardian]: https://github.com/lukaszb/django-guardian [2.2-announcement]: ../topics/2.2-announcement.md +[filtering]: filtering.md From 23fbbb1e164360287b775ab33da321a29136b2a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 08:57:59 +0000 Subject: [PATCH 013/128] Drop `six` module from coverage. --- rest_framework/runtests/runcoverage.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py index bcab1d148..ce11b213e 100755 --- a/rest_framework/runtests/runcoverage.py +++ b/rest_framework/runtests/runcoverage.py @@ -52,12 +52,15 @@ def main(): if os.path.basename(path) in ['tests', 'runtests', 'migrations']: continue - # Drop the compat module from coverage, since we're not interested in the coverage - # of a module which is specifically for resolving environment dependant imports. + # Drop the compat and six modules from coverage, since we're not interested in the coverage + # of modules which are specifically for resolving environment dependant imports. # (Because we'll end up getting different coverage reports for it for each environment) if 'compat.py' in files: files.remove('compat.py') + if 'six.py' in files: + files.remove('six.py') + # Same applies to template tags module. # This module has to include branching on Django versions, # so it's never possible for it to have full coverage. From 36cdefbb4d689e511aa53b46f05ca29106960847 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 08:58:12 +0000 Subject: [PATCH 014/128] Notes on object-level permissions. --- docs/topics/2.2-announcement.md | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 4ed9f9706..262ae61d2 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -101,6 +101,45 @@ In keeping with Django's CharField API, REST framework's `CharField` will only e The `blank` keyword argument will continue to function, but will raise a `PendingDeprecationWarning`. +### Simpler object-level permissions + +Custom permissions classes previously used the signatute `.has_permission(self, request, view, obj=None)`. This method would be called twice, firstly for the global permissions check, with the `obj` parameter set to `None`, and again for the object-level permissions check when appropriate, with the `obj` parameter set to the relevant model instance. + +The global permissions check and object-level permissions check are now seperated into two seperate methods, which gives a cleaner, more obvious API. + +* Global permission checks now use the `.has_permission(self, request, view)` signature. +* Object-level permission checks use a new method `.has_object_permission(self, request, view, obj)`. + +For example, the following custom permission class: + + class IsOwner(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to view or edit it. + Model instances are expected to include an `owner` attribute. + """ + + def has_permission(self, request, view, obj=None): + if obj is None: + # Ignore global permissions check + return True + + return obj.owner == request.user + +Now becomes: + + class IsOwner(permissions.BasePermission): + """ + Custom permission to only allow owners of an object to view or edit it. + Model instances are expected to include an `owner` attribute. + """ + + def has_object_permission(self, request, view, obj): + return obj.owner == request.user + +If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but it's use will raise a `PendingDeprecationWarning`. + +Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions. + [xordoquy]: https://github.com/xordoquy [django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ From 112753917d7a9a1effe6e64d9344de3466425733 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 12:14:29 +0000 Subject: [PATCH 015/128] Update release notes --- docs/topics/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4317b83c0..63f8539ad 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -28,8 +28,14 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Python 3 support. * Added a `post_save()` hook to the generic views. * Allow serializers to handle dicts as well as objects. +* Deprecate `ManyRelatedField()` syntax in favor of `RelatedField(many=True)` +* Deprecate `null=True` on relations in favor of `required=False`. +* Deprecate `blank=True` on CharFields, just use `required=False`. +* Deprecate optional `obj` argument in permissions checks in favor of `has_object_permission`. +* Bugfix: Allow serializer output to be cached. * Bugfix: Fix styling on browsable API login. * Bugfix: Fix issue with deserializing empty to-many relations. * Bugfix: Ensure model field validation is still applied for ModelSerializer subclasses with an custom `.restore_object()` method. From f642ee48a666d5cfc3a15cf8c33629bbb6173787 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 12:14:58 +0000 Subject: [PATCH 016/128] Document serializing querysets --- docs/api-guide/serializers.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 487502e9a..027c343c6 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -80,6 +80,15 @@ By default, serializers must be passed values for all required fields or they wi serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data +## Serializing querysets + +To serialize a queryset instead of an object instance, you should pass the `many=True` flag when instantiating the serializer. + + queryset = Comment.objects.all() + serializer = CommentSerializer(queryset, many=True) + serializer.data + # [{'email': u'leila@example.com', 'content': u'foo bar', 'created': datetime.datetime(2012, 8, 22, 16, 20, 9, 822774)}, {'email': u'jamie@example.com', 'content': u'baz', 'created': datetime.datetime(2013, 1, 12, 16, 12, 45, 104445)}] + ## Validation When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. From 388e6173669a295214a718e55dbf467559835dee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 13:53:45 +0000 Subject: [PATCH 017/128] Raise warnings on implicit many serialization --- rest_framework/serializers.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0a2e103f9..d59bcfd3f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -344,6 +344,10 @@ class BaseSerializer(Field): many = self.many else: many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict)) + if many: + warnings.warn('Implict list/queryset serialization is due to be deprecated. ' + 'Use the `many=True` flag when instantiating the serializer.', + PendingDeprecationWarning, stacklevel=2) # TODO: error data when deserializing lists if many: @@ -369,6 +373,10 @@ class BaseSerializer(Field): many = self.many else: many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) + if many: + warnings.warn('Implict list/queryset serialization is due to be deprecated. ' + 'Use the `many=True` flag when instantiating the serializer.', + PendingDeprecationWarning, stacklevel=2) if many: self._data = [self.to_native(item) for item in obj] From 41ac1e8f32491f50a5c784bb1cd1cfdba52f7072 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 13:54:50 +0000 Subject: [PATCH 018/128] Raise warnings if 'request' not in context for hyperlinked fields. --- rest_framework/relations.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 53fd646d0..24e8e15cf 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -311,6 +311,13 @@ class HyperlinkedRelatedField(RelatedField): view_name = self.view_name request = self.context.get('request', None) format = self.format or self.context.get('format', None) + + if request is None: + warnings.warn("Using `HyperlinkedRelatedField` without including the " + "request in the serializer context is due to be deprecated. " + "Add `context={'request': request}` when instantiating the serializer.", + PendingDeprecationWarning, stacklevel=4) + pk = getattr(obj, 'pk', None) if pk is None: return @@ -420,6 +427,12 @@ class HyperlinkedIdentityField(Field): view_name = self.view_name or self.parent.opts.view_name kwargs = {self.pk_url_kwarg: obj.pk} + if request is None: + warnings.warn("Using `HyperlinkedIdentityField` without including the " + "request in the serializer context is due to be deprecated. " + "Add `context={'request': request}` when instantiating the serializer.", + PendingDeprecationWarning, stacklevel=4) + # By default use whatever format is given for the current context # unless the target is a different type to the source. # From f97aa49809d1a1788c8d524bf274a9b65f740c64 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 13:55:00 +0000 Subject: [PATCH 019/128] Docs on serializer context. --- docs/api-guide/serializers.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 027c343c6..85189434f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -123,7 +123,7 @@ To do any other validation that requires access to multiple fields, add a method from rest_framework import serializers class EventSerializer(serializers.Serializer): - description = serializers.CahrField(max_length=100) + description = serializers.CharField(max_length=100) start = serializers.DateTimeField() finish = serializers.DateTimeField() @@ -164,6 +164,17 @@ The `Serializer` class is itself a type of `Field`, and can be used to represent --- +## Including extra context + +There are some cases where you need to provide extra context to the serializer in addition to the object being serialized. One common case is if you're using a serializer that includes hyperlinked relations, which requires the serializer to have access to the current request so that it can properly generate fully qualified URLs. + +You can provide arbitrary additional context by passing a `context` argument when instantiating the serializer. For example: + + serializer = AccountSerializer(account, context={'request': request}) + serializer.data + # {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'} + +The context dictionary can be used within any serializer field logic, such as a custom `.to_native()` method, by accessing the `self.context` attribute. ## Creating custom fields From 7f797abc19dff6972ebf41a2211daeb30cadff46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 20:07:35 +0000 Subject: [PATCH 020/128] Remove deprecated APIs from tests --- rest_framework/serializers.py | 2 +- rest_framework/tests/genericrelations.py | 2 +- rest_framework/tests/relations_hyperlink.py | 225 ++++++++++---------- rest_framework/tests/relations_nested.py | 8 +- rest_framework/tests/relations_pk.py | 40 ++-- rest_framework/tests/relations_slug.py | 24 +-- rest_framework/tests/serializer.py | 2 +- 7 files changed, 155 insertions(+), 148 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d59bcfd3f..11cce4d64 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -347,7 +347,7 @@ class BaseSerializer(Field): if many: warnings.warn('Implict list/queryset serialization is due to be deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', - PendingDeprecationWarning, stacklevel=2) + PendingDeprecationWarning, stacklevel=3) # TODO: error data when deserializing lists if many: diff --git a/rest_framework/tests/genericrelations.py b/rest_framework/tests/genericrelations.py index 029564d02..52b47f976 100644 --- a/rest_framework/tests/genericrelations.py +++ b/rest_framework/tests/genericrelations.py @@ -82,7 +82,7 @@ class TestGenericRelations(TestCase): model = Tag exclude = ('id', 'content_type', 'object_id') - serializer = TagSerializer(Tag.objects.all()) + serializer = TagSerializer(Tag.objects.all(), many=True) expected = [ { 'tag': 'django', diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 89b2aec13..e806ddd7e 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -1,8 +1,15 @@ from __future__ import unicode_literals from django.test import TestCase +from django.test.client import RequestFactory from rest_framework import serializers from rest_framework.compat import patterns, url -from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource +from rest_framework.tests.models import ( + ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, + NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource +) + +factory = RequestFactory() +request = factory.get('/') # Just to ensure we have a request in the serializer context def dummy_view(request, pk): @@ -73,64 +80,64 @@ class HyperlinkedManyToManyTests(TestCase): def test_many_to_many_retrieve(self): queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/']}, - {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']}, + {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']}, + {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_retrieve(self): queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']} + {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) def test_many_to_many_update(self): - data = {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']} instance = ManyToManySource.objects.get(pk=1) - serializer = ManyToManySourceSerializer(instance, data=data) + serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) serializer.save() self.assertEquals(serializer.data, data) # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, - {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}, + {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']}, + {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_update(self): - data = {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/']} + data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']} instance = ManyToManyTarget.objects.get(pk=1) - serializer = ManyToManyTargetSerializer(instance, data=data) + serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) serializer.save() self.assertEquals(serializer.data, data) # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/']}, - {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']} + {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}, + {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) def test_many_to_many_create(self): - data = {'url': '/manytomanysource/4/', 'name': 'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} - serializer = ManyToManySourceSerializer(data=data) + data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']} + serializer = ManyToManySourceSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -138,18 +145,18 @@ class HyperlinkedManyToManyTests(TestCase): # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanysource/1/', 'name': 'source-1', 'targets': ['/manytomanytarget/1/']}, - {'url': '/manytomanysource/2/', 'name': 'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, - {'url': '/manytomanysource/3/', 'name': 'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, - {'url': '/manytomanysource/4/', 'name': 'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} + {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']}, + {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']}, + {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}, + {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']} ] self.assertEquals(serializer.data, expected) def test_reverse_many_to_many_create(self): - data = {'url': '/manytomanytarget/4/', 'name': 'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} - serializer = ManyToManyTargetSerializer(data=data) + data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']} + serializer = ManyToManyTargetSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -157,12 +164,12 @@ class HyperlinkedManyToManyTests(TestCase): # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/manytomanytarget/1/', 'name': 'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/2/', 'name': 'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, - {'url': '/manytomanytarget/3/', 'name': 'target-3', 'sources': ['/manytomanysource/3/']}, - {'url': '/manytomanytarget/4/', 'name': 'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} + {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}, + {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']} ] self.assertEquals(serializer.data, expected) @@ -181,60 +188,60 @@ class HyperlinkedForeignKeyTests(TestCase): def test_foreign_key_retrieve(self): queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'} + {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'} ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, - {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, + {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']}, + {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update(self): - data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/2/'} + data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'} instance = ForeignKeySource.objects.get(pk=1) - serializer = ForeignKeySourceSerializer(instance, data=data) + serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/2/'}, - {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'} + {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}, + {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_incorrect_type(self): - data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': 2} + data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) - serializer = ForeignKeySourceSerializer(instance, data=data) + serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request}) self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']}) def test_reverse_foreign_key_update(self): - data = {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']} instance = ForeignKeyTarget.objects.get(pk=2) - serializer = ForeignKeyTargetSerializer(instance, data=data) + serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) # We shouldn't have saved anything to the db yet since save # hasn't been called. queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset) + new_serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, - {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, + {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']}, + {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, ] self.assertEquals(new_serializer.data, expected) @@ -243,16 +250,16 @@ class HyperlinkedForeignKeyTests(TestCase): # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/2/']}, - {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']}, + {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create(self): - data = {'url': '/foreignkeysource/4/', 'name': 'source-4', 'target': '/foreignkeytarget/2/'} - serializer = ForeignKeySourceSerializer(data=data) + data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'} + serializer = ForeignKeySourceSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -260,18 +267,18 @@ class HyperlinkedForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/3/', 'name': 'source-3', 'target': '/foreignkeytarget/1/'}, - {'url': '/foreignkeysource/4/', 'name': 'source-4', 'target': '/foreignkeytarget/2/'}, + {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}, ] self.assertEquals(serializer.data, expected) def test_reverse_foreign_key_create(self): - data = {'url': '/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} - serializer = ForeignKeyTargetSerializer(data=data) + data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']} + serializer = ForeignKeyTargetSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -279,18 +286,18 @@ class HyperlinkedForeignKeyTests(TestCase): # Ensure target 4 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['/foreignkeysource/2/']}, - {'url': '/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, - {'url': '/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']}, + {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, + {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_invalid_null(self): - data = {'url': '/foreignkeysource/1/', 'name': 'source-1', 'target': None} + data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) - serializer = ForeignKeySourceSerializer(instance, data=data) + serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request}) self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'target': ['This field is required.']}) @@ -309,17 +316,17 @@ class HyperlinkedNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) def test_foreign_key_create_with_valid_null(self): - data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) + data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -327,12 +334,12 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} + {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -341,9 +348,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''} - expected_data = {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) + data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''} + expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) @@ -351,30 +358,30 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - {'url': '/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} + {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_valid_null(self): - data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} + data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) + serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, - {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) @@ -383,21 +390,21 @@ class HyperlinkedNullableForeignKeyTests(TestCase): The emptystring should be interpreted as null in the context of relationships. """ - data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''} - expected_data = {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} + data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''} + expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) + serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request}) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, expected_data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, - {'url': '/nullableforeignkeysource/2/', 'name': 'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, + {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, + {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) @@ -415,7 +422,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # # Ensure target 1 is updated, and everything else is as expected # queryset = ForeignKeyTarget.objects.all() - # serializer = ForeignKeyTargetSerializer(queryset) + # serializer = ForeignKeyTargetSerializer(queryset, many=True) # expected = [ # {'id': 1, 'name': 'target-1', 'sources': [1]}, # {'id': 2, 'name': 'target-2', 'sources': []}, @@ -436,9 +443,9 @@ class HyperlinkedNullableOneToOneTests(TestCase): def test_reverse_foreign_key_retrieve_with_null(self): queryset = OneToOneTarget.objects.all() - serializer = NullableOneToOneTargetSerializer(queryset) + serializer = NullableOneToOneTargetSerializer(queryset, many=True, context={'request': request}) expected = [ - {'url': '/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': '/nullableonetoonesource/1/'}, - {'url': '/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None}, + {'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'}, + {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 6f42dd5d0..563d1d4d4 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -52,7 +52,7 @@ class ReverseForeignKeyTests(TestCase): def test_foreign_key_retrieve(self): queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, @@ -62,7 +62,7 @@ class ReverseForeignKeyTests(TestCase): def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [ {'id': 1, 'name': 'source-1', 'target': 1}, @@ -87,7 +87,7 @@ class NestedNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}}, {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}}, @@ -107,7 +107,7 @@ class NestedNullableOneToOneTests(TestCase): def test_reverse_foreign_key_retrieve_with_null(self): queryset = OneToOneTarget.objects.all() - serializer = NullableOneToOneTargetSerializer(queryset) + serializer = NullableOneToOneTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}}, {'id': 2, 'name': 'target-2', 'nullable_source': None}, diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index b8166d681..130e90938 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -55,7 +55,7 @@ class PKManyToManyTests(TestCase): def test_many_to_many_retrieve(self): queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'targets': [1]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, @@ -65,7 +65,7 @@ class PKManyToManyTests(TestCase): def test_reverse_many_to_many_retrieve(self): queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, @@ -83,7 +83,7 @@ class PKManyToManyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, @@ -101,7 +101,7 @@ class PKManyToManyTests(TestCase): # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [1]}, {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, @@ -119,7 +119,7 @@ class PKManyToManyTests(TestCase): # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() - serializer = ManyToManySourceSerializer(queryset) + serializer = ManyToManySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'targets': [1]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, @@ -138,7 +138,7 @@ class PKManyToManyTests(TestCase): # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset) + serializer = ManyToManyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, @@ -160,7 +160,7 @@ class PKForeignKeyTests(TestCase): def test_foreign_key_retrieve(self): queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 1}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -170,7 +170,7 @@ class PKForeignKeyTests(TestCase): def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -187,7 +187,7 @@ class PKForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 2}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -210,7 +210,7 @@ class PKForeignKeyTests(TestCase): # We shouldn't have saved anything to the db yet since save # hasn't been called. queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset) + new_serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -222,7 +222,7 @@ class PKForeignKeyTests(TestCase): # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [2]}, {'id': 2, 'name': 'target-2', 'sources': [1, 3]}, @@ -239,7 +239,7 @@ class PKForeignKeyTests(TestCase): # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 1}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -258,7 +258,7 @@ class PKForeignKeyTests(TestCase): # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': [2]}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -286,7 +286,7 @@ class PKNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 1}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -304,7 +304,7 @@ class PKNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 1}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -328,7 +328,7 @@ class PKNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 1}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -347,7 +347,7 @@ class PKNullableForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': None}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -370,7 +370,7 @@ class PKNullableForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': None}, {'id': 2, 'name': 'source-2', 'target': 1}, @@ -392,7 +392,7 @@ class PKNullableForeignKeyTests(TestCase): # # Ensure target 1 is updated, and everything else is as expected # queryset = ForeignKeyTarget.objects.all() - # serializer = ForeignKeyTargetSerializer(queryset) + # serializer = ForeignKeyTargetSerializer(queryset, many=True) # expected = [ # {'id': 1, 'name': 'target-1', 'sources': [1]}, # {'id': 2, 'name': 'target-2', 'sources': []}, @@ -411,7 +411,7 @@ class PKNullableOneToOneTests(TestCase): def test_reverse_foreign_key_retrieve_with_null(self): queryset = OneToOneTarget.objects.all() - serializer = NullableOneToOneTargetSerializer(queryset) + serializer = NullableOneToOneTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'nullable_source': 1}, {'id': 2, 'name': 'target-2', 'nullable_source': None}, diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index a325f6eaf..65d226b1e 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -37,7 +37,7 @@ class PKForeignKeyTests(TestCase): def test_foreign_key_retrieve(self): queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-1'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -47,7 +47,7 @@ class PKForeignKeyTests(TestCase): def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -64,7 +64,7 @@ class PKForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-2'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -87,7 +87,7 @@ class PKForeignKeyTests(TestCase): # We shouldn't have saved anything to the db yet since save # hasn't been called. queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset) + new_serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -99,7 +99,7 @@ class PKForeignKeyTests(TestCase): # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}, @@ -117,7 +117,7 @@ class PKForeignKeyTests(TestCase): # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset) + serializer = ForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-1'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -136,7 +136,7 @@ class PKForeignKeyTests(TestCase): # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) + serializer = ForeignKeyTargetSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, {'id': 2, 'name': 'target-2', 'sources': []}, @@ -164,7 +164,7 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-1'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -182,7 +182,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-1'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -206,7 +206,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': 'target-1'}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -225,7 +225,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': None}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, @@ -248,7 +248,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset, many=True) expected = [ {'id': 1, 'name': 'source-1', 'target': None}, {'id': 2, 'name': 'source-2', 'target': 'target-1'}, diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 193795521..f269d1a74 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -261,7 +261,7 @@ class ValidationTests(TestCase): Data of the wrong type is not valid. """ data = ['i am', 'a', 'list'] - serializer = CommentSerializer(self.comment, data=data) + serializer = CommentSerializer(self.comment, data=data, many=True) self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'non_field_errors': ['Invalid data']}) From edaf031935ae04db48d078452d46c71b6b5d7ebe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 20:14:47 +0000 Subject: [PATCH 021/128] Notes on explicit hyperlink relations behavior --- docs/topics/2.2-announcement.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 262ae61d2..65c05267b 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -140,6 +140,12 @@ If you're overriding the `BasePermission` class, the old-style signature will co Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions. +## More explicit hyperlink relations behavior + +When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not. + +From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but it's use will raise a `PendingDeprecationWarning`. + [xordoquy]: https://github.com/xordoquy [django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ From a08fa7c71ed026cad0ec9832b34f95fe38725e53 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:05:17 +0000 Subject: [PATCH 022/128] Add deprecation policy to release notes. --- docs/topics/release-notes.md | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 63f8539ad..8756430f4 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -8,9 +8,23 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. -Medium version numbers (0.x.0) may include minor API changes. You should read the release notes carefully before upgrading between medium point releases. +Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases. -Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned. +Major version numbers (x.0.0) are reserved for substantial project milestones. No major point releases are currently planned. + +## Deprecation policy + +REST framework releases follow a formal deprecation policy, which is in line with [Django's deprecation policy][django-deprecation-policy]. + +The timeline for deprecation of a feature present in version 1.0 would work as follows: + +* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `PendingDeprecationWarning` warnings if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. + +* Version 1.2 would escalate these warnings to `DeprecationWarning`, which is loud by default. + +* Version 1.3 would remove the deprecated bits of API entirely. + +Note that in line with Django's policy, any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change. ## Upgrading @@ -24,9 +38,11 @@ You can determine your currently installed version using `pip freeze`: --- -## 2.1.x series +## 2.2.x series -### Master +### 2.2.0 + +**Date**: 13th Feb 2013 * Python 3 support. * Added a `post_save()` hook to the generic views. @@ -35,11 +51,18 @@ You can determine your currently installed version using `pip freeze`: * Deprecate `null=True` on relations in favor of `required=False`. * Deprecate `blank=True` on CharFields, just use `required=False`. * Deprecate optional `obj` argument in permissions checks in favor of `has_object_permission`. +* Deprecate implicit hyperlinked relations behavior. * Bugfix: Allow serializer output to be cached. * Bugfix: Fix styling on browsable API login. * Bugfix: Fix issue with deserializing empty to-many relations. * Bugfix: Ensure model field validation is still applied for ModelSerializer subclasses with an custom `.restore_object()` method. +**Note**: See the [2.2 announcement][2.2-announcement] for full details. + +--- + +## 2.1.x series + ### 2.1.17 **Date**: 26th Jan 2013 @@ -356,6 +379,9 @@ This change will not affect user code, so long as it's following the recommended * Initial release. [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html +[deprecation-policy]: #deprecation-policy +[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[2.2-announcement]: 2.2-announcement.md [staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag [staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion From 52aa5be883e6714538c091f6bcafbb8e7a56a81c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:05:28 +0000 Subject: [PATCH 023/128] Remove changelog from README --- README.md | 194 +----------------------------------------------------- 1 file changed, 1 insertion(+), 193 deletions(-) diff --git a/README.md b/README.md index 62131ac68..861c677cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Author:** Tom Christie. [Follow me on Twitter][twitter]. -**Support:** [REST framework discussion group][group]. +**Support:** [REST framework group][group], or `#restframework` on freenode IRC. [![build-status-image]][travis] @@ -77,198 +77,6 @@ To run the tests. ./rest_framework/runtests/runtests.py -# Changelog - -### 2.1.17 - -**Date**: 26th Jan 2013 - -* Support proper 401 Unauthorized responses where appropriate, instead of always using 403 Forbidden. -* Support json encoding of timedelta objects. -* `format_suffix_patterns()` now supports `include` style URL patterns. -* Bugfix: Fix issues with custom pagination serializers. -* Bugfix: Nested serializers now accept `source='*'` argument. -* Bugfix: Return proper validation errors when incorrect types supplied for relational fields. -* Bugfix: Support nullable FKs with `SlugRelatedField`. -* Bugfix: Don't call custom validation methods if the field has an error. - -**Note**: If the primary authentication class is `TokenAuthentication` or `BasicAuthentication`, a view will now correctly return 401 responses to unauthenticated access, with an appropriate `WWW-Authenticate` header, instead of 403 responses. - -### 2.1.16 - -**Date**: 14th Jan 2013 - -* Deprecate django.utils.simplejson in favor of Python 2.6's built-in json module. -* Bugfix: `auto_now`, `auto_now_add` and other `editable=False` fields now default to read-only. -* Bugfix: PK fields now only default to read-only if they are an AutoField or if `editable=False`. -* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types. -* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types. -* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one - -### 2.1.15 - -**Date**: 3rd Jan 2013 - -* Added `PATCH` support. -* Added `RetrieveUpdateAPIView`. -* Relation changes are now persisted in `.save` instead of in `.restore_object`. -* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`. -* Tweak behavior of hyperlinked fields with an explicit format suffix. -* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None. -* Bugfix: Partial updates should not set default values if field is not included. - -### 2.1.14 - -**Date**: 31st Dec 2012 - -* Bugfix: ModelSerializers now include reverse FK fields on creation. -* Bugfix: Model fields with `blank=True` are now `required=False` by default. -* Bugfix: Nested serializers now support nullable relationships. - -**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`. - -This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`. - -### 2.1.13 - -**Date**: 28th Dec 2012 - -* Support configurable `STATICFILES_STORAGE` storage. -* Bugfix: Related fields now respect the required flag, and may be required=False. - -### 2.1.12 - -**Date**: 21st Dec 2012 - -* Bugfix: Fix bug that could occur using ChoiceField. -* Bugfix: Fix exception in browseable API on DELETE. -* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. - -### 2.1.11 - -**Date**: 17th Dec 2012 - -* Bugfix: Fix issue with M2M fields in browseable API. - -### 2.1.10 - -**Date**: 17th Dec 2012 - -* Bugfix: Ensure read-only fields don't have model validation applied. -* Bugfix: Fix hyperlinked fields in paginated results. - -### 2.1.9 - -**Date**: 11th Dec 2012 - -* Bugfix: Fix broken nested serialization. -* Bugfix: Fix `Meta.fields` only working as tuple not as list. -* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. - -### 2.1.8 - -**Date**: 8th Dec 2012 - -* Fix for creating nullable Foreign Keys with `''` as well as `None`. -* Added `null=` related field option. - -### 2.1.7 - -**Date**: 7th Dec 2012 - -* Serializers now properly support nullable Foreign Keys. -* Serializer validation now includes model field validation, such as uniqueness constraints. -* Support 'true' and 'false' string values for BooleanField. -* Added pickle support for serialized data. -* Support `source='dotted.notation'` style for nested serializers. -* Make `Request.user` settable. -* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer` - -### 2.1.6 - -**Date**: 23rd Nov 2012 - -* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.) - -### 2.1.5 - -**Date**: 23rd Nov 2012 - -* Bugfix: Fix DjangoModelPermissions. - -### 2.1.4 - -**Date**: 22nd Nov 2012 - -* Support for partial updates with serializers. -* Added `RegexField`. -* Added `SerializerMethodField`. -* Serializer performance improvements. -* Added `obtain_token_view` to get tokens when using `TokenAuthentication`. -* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`. - -### 2.1.3 - -**Date**: 16th Nov 2012 - -* Added `FileField` and `ImageField`. For use with `MultiPartParser`. -* Added `URLField` and `SlugField`. -* Support for `read_only_fields` on `ModelSerializer` classes. -* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view. -* 201 Responses now return a 'Location' header. -* Bugfix: Serializer fields now respect `max_length`. - -### 2.1.2 - -**Date**: 9th Nov 2012 - -* **Filtering support.** -* Bugfix: Support creation of objects with reverse M2M relations. - -### 2.1.1 - -**Date**: 7th Nov 2012 - -* Support use of HTML exception templates. Eg. `403.html` -* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments. -* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs. -* Bugfix: Make textareas same width as other fields in browsable API. -* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. - -### 2.1.0 - -**Date**: 5th Nov 2012 - -**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. - -* **Serializer `instance` and `data` keyword args have their position swapped.** -* `queryset` argument is now optional on writable model fields. -* Hyperlinked related fields optionally take `slug_field` and `slug_field_kwarg` arguments. -* Support Django's cache framework. -* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.) -* Bugfixes (Support choice field in Browseable API) - -### 2.0.2 - -**Date**: 2nd Nov 2012 - -* Fix issues with pk related fields in the browsable API. - -### 2.0.1 - -**Date**: 1st Nov 2012 - -* Add support for relational fields in the browsable API. -* Added SlugRelatedField and ManySlugRelatedField. -* If PUT creates an instance return '201 Created', instead of '200 OK'. - -### 2.0.0 - -**Date**: 30th Oct 2012 - -* Redesign of core components. -* **Fix all of the things**. - # License Copyright (c) 2011-2013, Tom Christie From 891b197f0b5eb19fc86f3c1ced5c9b49749afeb6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:06:13 +0000 Subject: [PATCH 024/128] Drop issue management section --- docs/topics/2.2-announcement.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 65c05267b..9523eebf9 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -30,10 +30,6 @@ As of the 2.2 merge, we've also hit an impressive milestone. The number of comm Our [mailing list][mailing-list] and #restframework IRC channel are also very active, and we've got a really impressive rate of development both on REST framework itself, and on third party packages such as the great [django-rest-framework-docs][django-rest-framework-docs] package from [Marc Gibbons][marcgibbons]. -## Issue management - -All the design work that went into version 2 of Django REST framework has made keeping on top of issues much easier. We've been super-focused on keeping the [issues list][issues] strictly under control, and we've hit another important milestone. At the point of releasing 2.2 there are currently **no open 'bug' tickets**, and the plan is to keep it that way for as much of the time as possible. - ## API changes The 2.2 release makes a few changes to the serializer fields API, in order to make it more consistent, simple, and easier to use. From 3f529dc25d66fba0c6f94944ebc92f338c86434d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:06:20 +0000 Subject: [PATCH 025/128] Typo --- docs/topics/2.2-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 9523eebf9..8a7ae326a 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -16,7 +16,7 @@ We've now introduced an official deprecation policy, which is in line with [Djan The timeline for deprecation works as follows: -* Version 2.2 introduces some API changes as detailed in the release notes. It remains fully backwards compatible with 2.1, but will raise `PendingDeprecationWarning` warnings if you use bits API that are due to be deprecated. These warnings are silent by default, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. +* Version 2.2 introduces some API changes as detailed in the release notes. It remains fully backwards compatible with 2.1, but will raise `PendingDeprecationWarning` warnings if you use bits of API that are due to be deprecated. These warnings are silent by default, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. * Version 2.3 will escalate these warnings to `DeprecationWarning`, which is loud by default. From 724906c516b71f3ec5bc2005c66a6145c77c3739 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:58:38 +0000 Subject: [PATCH 026/128] Test for #637. --- rest_framework/tests/serializer.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index f269d1a74..5350a8f23 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -785,6 +785,23 @@ class RelatedTraversalTest(TestCase): self.assertEqual(serializer.data, expected) + def test_queryset_nested_traversal(self): + """ + Relational fields should be able to use methods as their source. + """ + BlogPost.objects.create(title='blah') + + class QuerysetMethodSerializer(serializers.Serializer): + blogposts = serializers.RelatedField(many=True, source='get_all_blogposts') + + class ClassWithQuerysetMethod(object): + def get_all_blogposts(self): + return BlogPost.objects + + obj = ClassWithQuerysetMethod() + serializer = QuerysetMethodSerializer(obj) + self.assertEquals(serializer.data, {'blogposts': [u'BlogPost object']}) + class SerializerMethodFieldTests(TestCase): def setUp(self): From f505b2e4404a58c9d841ee96e560955eb545fede Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 22:59:01 +0000 Subject: [PATCH 027/128] Clean up field_to_native logic --- rest_framework/fields.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index aa6fa3abc..327008fb1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -94,12 +94,14 @@ class Field(object): if self.source == '*': return self.to_native(obj) - if self.source: - value = obj - for component in self.source.split('.'): - value = get_component(value, component) - else: - value = get_component(obj, field_name) + source = self.source or field_name + value = obj + + for component in source.split('.'): + value = get_component(value, component) + if value is None: + break + return self.to_native(value) def to_native(self, value): From 15fa42b647e1bac0a9a9309bd4ad2d810e55c9cd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:00:43 +0000 Subject: [PATCH 028/128] Unicode literal fix --- rest_framework/tests/serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 5350a8f23..799ca108c 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -801,6 +801,7 @@ class RelatedTraversalTest(TestCase): obj = ClassWithQuerysetMethod() serializer = QuerysetMethodSerializer(obj) self.assertEquals(serializer.data, {'blogposts': [u'BlogPost object']}) + self.assertEquals(serializer.data, {'blogposts': ['BlogPost object']}) class SerializerMethodFieldTests(TestCase): From f341ead49944050b7902254ed4c89b3c9c11a018 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:01:20 +0000 Subject: [PATCH 029/128] Test for None in 'dotted.source' component. Closes #643. --- rest_framework/tests/serializer.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 799ca108c..da1101386 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -747,6 +747,9 @@ class ManyRelatedTests(TestCase): class RelatedTraversalTest(TestCase): def test_nested_traversal(self): + """ + Source argument should support dotted.source notation. + """ user = Person.objects.create(name="django") post = BlogPost.objects.create(title="Test blog post", writer=user) post.blogpostcomment_set.create(text="I love this blog post") @@ -785,6 +788,24 @@ class RelatedTraversalTest(TestCase): self.assertEqual(serializer.data, expected) + def test_nested_traversal_with_none(self): + """ + If a component of the dotted.source is None, return None for the field. + """ + from rest_framework.tests.models import NullableForeignKeySource + instance = NullableForeignKeySource.objects.create(name='Source with null FK') + + class NullableSourceSerializer(serializers.Serializer): + target_name = serializers.Field(source='target.name') + + serializer = NullableSourceSerializer(instance=instance) + + expected = { + 'target_name': None, + } + + self.assertEqual(serializer.data, expected) + def test_queryset_nested_traversal(self): """ Relational fields should be able to use methods as their source. @@ -800,7 +821,6 @@ class RelatedTraversalTest(TestCase): obj = ClassWithQuerysetMethod() serializer = QuerysetMethodSerializer(obj) - self.assertEquals(serializer.data, {'blogposts': [u'BlogPost object']}) self.assertEquals(serializer.data, {'blogposts': ['BlogPost object']}) From 018298deb89628b39e1caeceecb414c1e27310da Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:03:04 +0000 Subject: [PATCH 030/128] Relational fields use same field_to_native logic as regular fields. Fixes #637. Closes #638. --- rest_framework/relations.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 24e8e15cf..0c108717f 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -5,7 +5,7 @@ from django import forms from django.forms import widgets from django.forms.models import ModelChoiceIterator from django.utils.translation import ugettext_lazy as _ -from rest_framework.fields import Field, WritableField +from rest_framework.fields import Field, WritableField, get_component from rest_framework.reverse import reverse from rest_framework.compat import urlparse from rest_framework.compat import smart_text @@ -116,7 +116,16 @@ class RelatedField(WritableField): def field_to_native(self, obj, field_name): try: - value = getattr(obj, self.source or field_name) + if self.source == '*': + return self.to_native(obj) + + source = self.source or field_name + value = obj + + for component in source.split('.'): + value = get_component(value, component) + if value is None: + break except ObjectDoesNotExist: return None From b7a5c4b050ab5bd0125e6f7af4e0546c98125282 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:13:48 +0000 Subject: [PATCH 031/128] Notes on creating a custom `obtain_auth_token` view. Fixes #641. --- docs/api-guide/authentication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 9c899f17c..8c1d11852 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -167,6 +167,8 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } +Note that the default `obtain_auth_token` view explicitly uses JSON requests and responses, rather than using default renderer and parser classes in your settings. If you need a customized version of the `obtain_auth_token` view, you can do so by overriding the `ObtainAuthToken` view class, and using that in your url conf instead. + ## SessionAuthentication This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. From 195adf6ed44e34acce08e306c6ced0340c28798d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:17:19 +0000 Subject: [PATCH 032/128] Update release notes. --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 8756430f4..06e45674f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -52,6 +52,7 @@ You can determine your currently installed version using `pip freeze`: * Deprecate `blank=True` on CharFields, just use `required=False`. * Deprecate optional `obj` argument in permissions checks in favor of `has_object_permission`. * Deprecate implicit hyperlinked relations behavior. +* Bugfix: Fix broken DjangoModelPermissions. * Bugfix: Allow serializer output to be cached. * Bugfix: Fix styling on browsable API login. * Bugfix: Fix issue with deserializing empty to-many relations. From 0b2adaa9425fae0e82dd94b1ec15a56b904eac53 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 12 Feb 2013 23:17:41 +0000 Subject: [PATCH 033/128] Drop note on object-level permissions. Not yet supported by DjangoModelPermissions. --- docs/api-guide/permissions.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index d47dbc35e..2db6ce1e3 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -100,8 +100,6 @@ The default behaviour can also be overridden to support custom model permissions To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. -The `DjangoModelPermissions` class also supports object-level permissions. Third-party authorization backends such as [django-guardian][guardian] that provide object-level permissions should work just fine with `DjangoModelPermissions` without any custom configuration required. - --- # Custom permissions From 77db00f449d88ca701b82e9f1d9924b3cc026638 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 09:05:16 +0000 Subject: [PATCH 034/128] Tweak title --- docs/topics/2.2-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 8a7ae326a..e24fc6154 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -1,4 +1,4 @@ -# REST framework 2.2 release notes +# REST framework 2.2 announcement The 2.2 release represents an important point for REST framework, with the addition of Python 3 support, and the introduction of an official deprecation policy. From 31f45907e559b379b662260032fdabaf7517db7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 11:42:57 +0000 Subject: [PATCH 035/128] Kick travis. Meh. --- rest_framework/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 80e2c4107..411e69a25 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -2,5 +2,6 @@ __version__ = '2.1.17' VERSION = __version__ # synonym + # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' From d7417022f34bdf86a5c52b3b2bfd083a5ff33efd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 11:51:03 +0000 Subject: [PATCH 036/128] Kick travis again. --- rest_framework/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 411e69a25..80e2c4107 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -2,6 +2,5 @@ __version__ = '2.1.17' VERSION = __version__ # synonym - # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' From b58e763287a235e93a9a64fe8952f2a3f1152729 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 12:33:04 +0000 Subject: [PATCH 037/128] Fix pk relations tests which were not running. --- rest_framework/tests/relations_pk.py | 5 +++-- rest_framework/tests/relations_slug.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 130e90938..bcbc2b3ea 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource +from rest_framework.compat import six class ManyToManyTargetSerializer(serializers.ModelSerializer): @@ -200,7 +201,7 @@ class PKForeignKeyTests(TestCase): instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': ['Incorrect type. Expected pk value, received str.']}) + self.assertEquals(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]}) def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} @@ -271,7 +272,7 @@ class PKForeignKeyTests(TestCase): instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': ['Value may not be null']}) + self.assertEquals(serializer.errors, {'target': ['This field is required.']}) class PKNullableForeignKeyTests(TestCase): diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index 65d226b1e..0c7421f33 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -25,7 +25,7 @@ class NullableForeignKeySourceSerializer(serializers.ModelSerializer): # TODO: M2M Tests, FKTests (Non-nulable), One2One -class PKForeignKeyTests(TestCase): +class SlugForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') target.save() From 72412b69f02201e7a86a1b02b56b3d9ddc26c66d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 12:36:05 +0000 Subject: [PATCH 038/128] Set PASSWORD_HASHERS to massively speed up tests (almost x10) --- rest_framework/runtests/settings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index dd5d9dc3c..03bfc2162 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -102,6 +102,15 @@ INSTALLED_APPS = ( STATIC_URL = '/static/' +PASSWORD_HASHERS = ( + 'django.contrib.auth.hashers.SHA1PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2PasswordHasher', + 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', + 'django.contrib.auth.hashers.BCryptPasswordHasher', + 'django.contrib.auth.hashers.MD5PasswordHasher', + 'django.contrib.auth.hashers.CryptPasswordHasher', +) + import django if django.VERSION < (1, 3): From f17bae8aacc8476a51ccad64bc521bc2e1a363c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 14:05:57 +0000 Subject: [PATCH 039/128] Version 2.2.0 --- 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 80e2c4107..e6f4c18ac 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.1.17' +__version__ = '2.2.0' VERSION = __version__ # synonym From f8eb222ec11df54a8fbae5cf6cb74fb0d4bddffe Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 14:08:56 +0000 Subject: [PATCH 040/128] Add links to 2.2 announcement --- docs/index.md | 2 ++ docs/template.html | 1 + 2 files changed, 3 insertions(+) diff --git a/docs/index.md b/docs/index.md index 37e7cb3cf..0059e17ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -116,6 +116,7 @@ General guides to using REST framework. * [The Browsable API][browsableapi] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [2.0 Announcement][rest-framework-2-announcement] +* [2.2 Announcement][2.2-announcement] * [Release Notes][release-notes] * [Credits][credits] @@ -211,6 +212,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [contributing]: topics/contributing.md [rest-framework-2-announcement]: topics/rest-framework-2-announcement.md +[2.2-announcement]: topics/2.2-announcement.md [release-notes]: topics/release-notes.md [credits]: topics/credits.md diff --git a/docs/template.html b/docs/template.html index 2a87e92ba..e0f88daf5 100644 --- a/docs/template.html +++ b/docs/template.html @@ -94,6 +94,7 @@
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • 2.0 Announcement
  • +
  • 2.2 Announcement
  • Release Notes
  • Credits
  • From 569dc67a1220f0b577e7873bf7ee3ac54cf60143 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 13 Feb 2013 14:40:02 +0000 Subject: [PATCH 041/128] Username tweak. --- docs/topics/credits.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b84f13576..e4abd286c 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -19,7 +19,7 @@ The following people have helped make REST framework great. * Craig Blaszczyk - [jakul] * Garcia Solero - [garciasolero] * Tom Drummond - [devioustree] -* Danilo Bargen - [gwrtheyrn] +* Danilo Bargen - [dbrgn] * Andrew McCloud - [amccloud] * Thomas Steinacher - [thomasst] * Meurig Freeman - [meurig] @@ -155,7 +155,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [jakul]: https://github.com/jakul [garciasolero]: https://github.com/garciasolero [devioustree]: https://github.com/devioustree -[gwrtheyrn]: https://github.com/gwrtheyrn +[dbrgn]: https://github.com/dbrgn [amccloud]: https://github.com/amccloud [thomasst]: https://github.com/thomasst [meurig]: https://github.com/meurig From 876bd67888c851826288d4c2a669c7def9956858 Mon Sep 17 00:00:00 2001 From: floppya Date: Wed, 13 Feb 2013 13:59:00 -0600 Subject: [PATCH 042/128] Minor doc fixes Fixes misspelling of "primitive" and removes an awkward "with". --- docs/api-guide/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 51c0fb4bd..13d4760a3 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -37,7 +37,7 @@ We could now return that data in a `Response` object, and it would be rendered i ## Paginating QuerySets -Our first example worked because we were using primative 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 with. +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. From 40b13a869b0c6bfbacf4498fc834dc9052d8b363 Mon Sep 17 00:00:00 2001 From: Diego Gaustein Date: Wed, 13 Feb 2013 20:34:23 -0300 Subject: [PATCH 043/128] Make is_simple_callable consider fields which have default arguments --- rest_framework/fields.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 327008fb1..abc5fd44c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -24,10 +24,14 @@ def is_simple_callable(obj): """ True if the object is a callable that takes no arguments. """ - return ( - (inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or - (inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1) - ) + try: + args, _, _, defaults = inspect.getargspec(obj) + except TypeError: + return False + else: + len_args = len(args) if inspect.isfunction(obj) else len(args) - 1 + len_defaults = len(defaults) if defaults else 0 + return len_args <= len_defaults def get_component(obj, attr_name): From 2eab7b9f59a9e4e7deedd823bdaf135e0a9b01a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 14 Feb 2013 09:04:09 +0100 Subject: [PATCH 044/128] thanks @floppya for docs fix --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e4abd286c..1320d4d4b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -102,6 +102,7 @@ The following people have helped make REST framework great. * Andrea de Marco - [z4r] * Fernando Rocha - [fernandogrd] * Xavier Ordoquy - [xordoquy] +* Adam Wentz - [floppya] Many thanks to everyone who's contributed to the project. @@ -238,3 +239,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [z4r]: https://github.com/z4r [fernandogrd]: https://github.com/fernandogrd [xordoquy]: https://github.com/xordoquy +[floppya]: https://github.com/floppya From 095c11891b24a77ce316e6c950d8bf678e9b5b70 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 10:17:09 +0000 Subject: [PATCH 045/128] Tweak PyPI classifiers. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index bd6026c60..dddec9694 100755 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ setup( url='http://django-rest-framework.org', download_url='http://pypi.python.org/pypi/rest_framework/', license='BSD', - description='A lightweight REST framework for Django.', + description='Web APIs for Django, made easy.', author='Tom Christie', author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=get_packages('rest_framework'), @@ -67,7 +67,7 @@ setup( test_suite='rest_framework.runtests.runtests.main', install_requires=[], classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', 'Intended Audience :: Developers', From 84f059758ec4684df80bd8c811f8e71f27b14773 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 10:18:18 +0000 Subject: [PATCH 046/128] Drop currently unused unicode_literals in setup. --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index dddec9694..ebaaf9828 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -#from __future__ import unicode_literals - from setuptools import setup import re import os From 34145408cb73213c2f6c1e9ab53795e6d7e3a3f6 Mon Sep 17 00:00:00 2001 From: eofs Date: Thu, 14 Feb 2013 13:21:54 +0200 Subject: [PATCH 047/128] Missing imports added --- docs/tutorial/1-serialization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index af5e83134..53d241365 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -109,7 +109,7 @@ The first thing we need to get started on our Web API is provide a way of serial from django.forms import widgets from rest_framework import serializers - from snippets.models import Snippet + from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES class SnippetSerializer(serializers.Serializer): @@ -119,9 +119,9 @@ The first thing we need to get started on our Web API is provide a way of serial code = serializers.CharField(widget=widgets.Textarea, max_length=100000) linenos = serializers.BooleanField(required=False) - language = serializers.ChoiceField(choices=models.LANGUAGE_CHOICES, + language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') - style = serializers.ChoiceField(choices=models.STYLE_CHOICES, + style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') def restore_object(self, attrs, instance=None): From 24ed0fa4b987a4a03b090963965e9865830c943f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 12:25:54 +0000 Subject: [PATCH 048/128] Drop accidental (uneeded) validation logic. --- rest_framework/serializers.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 11cce4d64..5d3475d41 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -591,12 +591,6 @@ class ModelSerializer(Serializer): else: instance = self.opts.model(**attrs) - try: - instance.full_clean(exclude=self.get_validation_exclusions()) - except ValidationError as err: - self._errors = err.message_dict - return None - return instance def from_native(self, data, files): From 9d3153ed04aed78a977e064d0715baaf178ff88a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 12:50:55 +0000 Subject: [PATCH 049/128] Fix broken clone_request --- rest_framework/request.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 47c009b2b..bde391f91 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -44,10 +44,11 @@ def clone_request(request, method): Internal helper method to clone a request, replacing with a different HTTP method. Used for checking permissions against other methods. """ - ret = Request(request._request, - request.parsers, - request.authenticators, - request.parser_context) + ret = Request(request=request._request, + parsers=request.parsers, + authenticators=request.authenticators, + negotiator=request.negotiator, + parser_context=request.parser_context) ret._data = request._data ret._files = request._files ret._content_type = request._content_type @@ -57,6 +58,8 @@ def clone_request(request, method): ret._user = request._user if hasattr(request, '_auth'): ret._auth = request._auth + if hasattr(request, '_authenticator'): + ret._authenticator = request._authenticator return ret From af686ec11a267183e84d8e59dca7d4ee1f05dedd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 13:02:28 +0000 Subject: [PATCH 050/128] request.DATA should use empty QueryDict for no data, not None. --- rest_framework/request.py | 9 +++++++-- rest_framework/tests/request.py | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index bde391f91..3e2fbd88e 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -11,7 +11,9 @@ The wrapped request then offers a richer API, in particular : """ from __future__ import unicode_literals from django.conf import settings +from django.http import QueryDict from django.http.multipartparser import parse_header +from django.utils.datastructures import MultiValueDict from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework.compat import BytesIO @@ -297,7 +299,9 @@ class Request(object): media_type = self.content_type if stream is None or media_type is None: - return (None, None) + empty_data = QueryDict('', self._request._encoding) + empty_files = MultiValueDict() + return (empty_data, empty_files) parser = self.negotiator.select_parser(self, self.parsers) @@ -311,7 +315,8 @@ class Request(object): try: return (parsed.data, parsed.files) except AttributeError: - return (parsed, None) + empty_files = MultiValueDict() + return (parsed, empty_files) def _authenticate(self): """ diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 9d4fdc7bd..4892f7a63 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -62,17 +62,17 @@ class TestMethodOverloading(TestCase): class TestContentParsing(TestCase): def test_standard_behaviour_determines_no_content_GET(self): """ - Ensure request.DATA returns None for GET request with no content. + Ensure request.DATA returns empty QueryDict for GET request. """ request = Request(factory.get('/')) - self.assertEqual(request.DATA, None) + self.assertEqual(request.DATA, {}) def test_standard_behaviour_determines_no_content_HEAD(self): """ - Ensure request.DATA returns None for HEAD request. + Ensure request.DATA returns empty QueryDict for HEAD request. """ request = Request(factory.head('/')) - self.assertEqual(request.DATA, None) + self.assertEqual(request.DATA, {}) def test_request_DATA_with_form_content(self): """ From 0d3e23f0d379f4df7ac7cd8f42cae2d303558852 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 13:02:38 +0000 Subject: [PATCH 051/128] Update release notes. --- docs/topics/release-notes.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 06e45674f..406923f4a 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`: ## 2.2.x series +### Master + +* Bugfix: request.DATA should return an empty `QueryDict` with no data, not `None`. +* Bugfix: Remove unneeded field validation, which caused extra querys. + ### 2.2.0 **Date**: 13th Feb 2013 From de029561d0cbb090c0d704811551b2d611472288 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 14 Feb 2013 13:09:42 +0000 Subject: [PATCH 052/128] Docs tweaks. --- docs/index.md | 2 +- docs/topics/release-notes.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0059e17ba..32b42419c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@

    - + Travis build image diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 406923f4a..3f3f87865 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,7 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * Bugfix: request.DATA should return an empty `QueryDict` with no data, not `None`. -* Bugfix: Remove unneeded field validation, which caused extra querys. +* Bugfix: Remove unneeded field validation, which caused extra queries. ### 2.2.0 From 5a5df18d182d43d993da8f0b5d4a8888e868fcae Mon Sep 17 00:00:00 2001 From: Andreas Pelme Date: Thu, 14 Feb 2013 21:19:51 +0100 Subject: [PATCH 053/128] Added a serializer TimeField --- docs/api-guide/fields.md | 8 +++++- rest_framework/compat.py | 2 +- rest_framework/fields.py | 28 ++++++++++++++++++++ rest_framework/serializers.py | 1 + rest_framework/tests/fields.py | 48 +++++++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 3f8a36e2a..8c28273b0 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -199,10 +199,16 @@ If you want to override this behavior, you'll need to declare the `DateTimeField class CommentSerializer(serializers.ModelSerializer): created = serializers.DateTimeField() - + class Meta: model = Comment +## TimeField + +A time representation. + +Corresponds to `django.db.models.fields.TimeField` + ## IntegerField An integer representation. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 9636b9c17..3fd865f85 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -349,7 +349,7 @@ except ImportError: # dateparse is ALSO new in Django 1.4 try: - from django.utils.dateparse import parse_date, parse_datetime + from django.utils.dateparse import parse_date, parse_datetime, parse_time except ImportError: import datetime import re diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 327008fb1..236e0f1e5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,6 +18,7 @@ from rest_framework.compat import timezone from rest_framework.compat import BytesIO from rest_framework.compat import six from rest_framework.compat import smart_text +from rest_framework.compat import parse_time def is_simple_callable(obj): @@ -531,6 +532,33 @@ class DateTimeField(WritableField): raise ValidationError(msg) +class TimeField(WritableField): + type_name = 'TimeField' + widget = widgets.TimeInput + form_field_class = forms.TimeField + + default_error_messages = { + 'invalid': _("'%s' value has an invalid format. It must be a valid " + "time in the HH:MM[:ss[.uuuuuu]] format."), + } + empty = None + + def from_native(self, value): + if value in validators.EMPTY_VALUES: + return None + + if isinstance(value, datetime.time): + return value + + try: + parsed = parse_time(value) + assert parsed is not None + return parsed + except ValueError: + msg = self.error_messages['invalid'] % value + raise ValidationError(msg) + + class IntegerField(WritableField): type_name = 'IntegerField' form_field_class = forms.IntegerField diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5d3475d41..b0372ab83 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -517,6 +517,7 @@ class ModelSerializer(Serializer): models.PositiveSmallIntegerField: IntegerField, models.DateTimeField: DateTimeField, models.DateField: DateField, + models.TimeField: TimeField, models.EmailField: EmailField, models.CharField: CharField, models.URLField: URLField, diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index b7587bf14..34f616781 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -2,8 +2,10 @@ General serializer field tests. """ from __future__ import unicode_literals +import datetime from django.db import models from django.test import TestCase +from django.core import validators from rest_framework import serializers @@ -26,7 +28,16 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): model = CharPrimaryKeyModel -class ReadOnlyFieldTests(TestCase): +class TimeFieldModel(models.Model): + clock = models.TimeField() + + +class TimeFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = TimeFieldModel + + +class BasicFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ auto_now and auto_now_add fields should be read_only by default. @@ -47,3 +58,38 @@ class ReadOnlyFieldTests(TestCase): """ serializer = CharPrimaryKeyModelSerializer() self.assertEquals(serializer.fields['id'].read_only, False) + + def test_TimeField_from_native(self): + f = serializers.TimeField() + result = f.from_native('12:34:56.987654') + + self.assertEqual(datetime.time(12, 34, 56, 987654), result) + + def test_TimeField_from_native_datetime_time(self): + """ + Make sure from_native() accepts a datetime.time instance. + """ + f = serializers.TimeField() + result = f.from_native(datetime.time(12, 34, 56)) + self.assertEqual(result, datetime.time(12, 34, 56)) + + def test_TimeField_from_native_empty(self): + f = serializers.TimeField() + result = f.from_native('') + self.assertEqual(result, None) + + def test_TimeField_from_native_invalid_time(self): + f = serializers.TimeField() + + try: + f.from_native('12:69:12') + except validators.ValidationError as e: + self.assertEqual(e.messages, ["'12:69:12' value has an invalid " + "format. It must be a valid time " + "in the HH:MM[:ss[.uuuuuu]] format."]) + else: + self.fail("ValidationError was not properly raised") + + def test_TimeFieldModelSerializer(self): + serializer = TimeFieldModelSerializer() + self.assertTrue(isinstance(serializer.fields['clock'], serializers.TimeField)) From 8fdf9250157cde2341ec9c86ead44b2ed1354aa2 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 14:41:12 +0600 Subject: [PATCH 054/128] Added tabs between object form and generic content form on PUT/PATCH form Some extra behaviour to `BrowsableAPIRenderer` to handle PATCH form. Added PATCH button on generic content PUT form. Tabs between object form and generic content form on PUT/PATCH form wich are both allways visible now. Fix #570 Refs #591 --- rest_framework/renderers.py | 10 ++- .../static/rest_framework/js/default.js | 2 + .../templates/rest_framework/base.html | 63 ++++++++++++------- .../templates/rest_framework/form.html | 13 ++++ 4 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 rest_framework/templates/rest_framework/form.html diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a65254042..736384d64 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -345,12 +345,11 @@ class BrowsableAPIRenderer(BaseRenderer): if not self.show_form_for_method(view, method, request, obj): return - if method == 'DELETE' or method == 'OPTIONS': + if method in ('DELETE', 'OPTIONS'): return True # Don't actually need to return a form if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) + return serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) @@ -422,14 +421,17 @@ class BrowsableAPIRenderer(BaseRenderer): view = renderer_context['view'] request = renderer_context['request'] response = renderer_context['response'] + media_types = [parser.media_type for parser in view.parser_classes] renderer = self.get_default_renderer(view) content = self.get_content(renderer, data, accepted_media_type, renderer_context) put_form = self.get_form(view, 'PUT', request) post_form = self.get_form(view, 'POST', request) + patch_form = self.get_form(view, 'PATCH', request) delete_form = self.get_form(view, 'DELETE', request) options_form = self.get_form(view, 'OPTIONS', request) + generic_content_form = self.get_generic_content_form(media_types) name = self.get_name(view) description = self.get_description(view) @@ -449,8 +451,10 @@ class BrowsableAPIRenderer(BaseRenderer): 'available_formats': [renderer.format for renderer in view.renderer_classes], 'put_form': put_form, 'post_form': post_form, + 'patch_form': patch_form, 'delete_form': delete_form, 'options_form': options_form, + 'generic_content_form': generic_content_form, 'api_settings': api_settings }) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index ecaccc0f0..bc5b02928 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -3,3 +3,5 @@ prettyPrint(); $('.js-tooltip').tooltip({ delay: 1000 }); + +$('#form-switcher a:first').tab('show'); \ No newline at end of file diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 8d807574b..87e5dc04e 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -147,32 +147,49 @@ {% endif %} - {% if put_form %} + {% if 'PUT' in allowed_methods or 'PATCH' in allowed_methods %}

    -
    -
    - - {% csrf_token %} - {{ put_form.non_field_errors }} - {% for field in put_form %} -
    - {{ field.label_tag|add_class:"control-label" }} -
    - {{ field }} - {{ field.help_text }} - -
    -
    - {% endfor %} -
    - -
    - -
    -
    + +
    + {% if put_form %} +
    + {% with form=put_form %} +
    +
    + {% include "rest_framework/form.html" %} +
    + +
    +
    +
    + {% endwith %} +
    + {% endif %} +
    + {% with form=generic_content_form %} +
    +
    + {% include "rest_framework/form.html" %} +
    + {% if 'PUT' in allowed_methods %} + + {% endif %} + {% if 'PATCH' in allowed_methods %} + + {% endif %} +
    +
    +
    + {% endwith %} +
    +
    {% endif %} - {% endif %} diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html new file mode 100644 index 000000000..dc7acc708 --- /dev/null +++ b/rest_framework/templates/rest_framework/form.html @@ -0,0 +1,13 @@ +{% load rest_framework %} +{% csrf_token %} +{{ form.non_field_errors }} +{% for field in form %} +
    + {{ field.label_tag|add_class:"control-label" }} +
    + {{ field }} + {{ field.help_text }} + +
    +
    +{% endfor %} From 725741198b9e185499662836b569cd729e1e9eb6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:05:55 +0000 Subject: [PATCH 055/128] Python 2.6.5+ required --- README.md | 2 +- docs/index.md | 2 +- docs/topics/2.2-announcement.md | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 861c677cf..7b7d1d47c 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ There is also a sandbox API you can use for testing purposes, [available here][s # Requirements -* Python (2.6, 2.7, 3.2, 3.3) +* Python (2.6.5+, 2.7, 3.2, 3.3) * Django (1.3, 1.4, 1.5) **Optional:** diff --git a/docs/index.md b/docs/index.md index 32b42419c..0188accf0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ There is also a sandbox API you can use for testing purposes, [available here][s REST framework requires the following: -* Python (2.6, 2.7, 3.2, 3.3) +* Python (2.6.5+, 2.7, 3.2, 3.3) * Django (1.3, 1.4, 1.5) The following packages are optional: diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index e24fc6154..0ef9fce84 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -30,9 +30,11 @@ As of the 2.2 merge, we've also hit an impressive milestone. The number of comm Our [mailing list][mailing-list] and #restframework IRC channel are also very active, and we've got a really impressive rate of development both on REST framework itself, and on third party packages such as the great [django-rest-framework-docs][django-rest-framework-docs] package from [Marc Gibbons][marcgibbons]. +--- + ## API changes -The 2.2 release makes a few changes to the serializer fields API, in order to make it more consistent, simple, and easier to use. +The 2.2 release makes a few changes to the API, in order to make it more consistent, simple, and easier to use. ### Cleaner to-many related fields @@ -136,7 +138,7 @@ If you're overriding the `BasePermission` class, the old-style signature will co Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions. -## More explicit hyperlink relations behavior +### More explicit hyperlink relations behavior When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not. From c1a40c58999a3ca3dd667d092051dd719fc0588b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:06:05 +0000 Subject: [PATCH 056/128] Drop unused import. --- rest_framework/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 55ad8cf35..1e481c874 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -13,7 +13,6 @@ from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings import re -import warnings def _remove_trailing_string(content, trailing): From e919cb1b57a27f581c07080e341a86421df78a88 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:09:34 +0000 Subject: [PATCH 057/128] Note python compatibility. --- docs/topics/2.2-announcement.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 0ef9fce84..d7164ce46 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -10,6 +10,8 @@ Django 1.6's Python 3 support is expected to be officially labeled as 'productio If you want to start ensuring that your own projects are Python 3 ready, we can highly recommend Django's [Porting to Python 3][porting-python-3] documentation. +Django REST framework's Python 2.6 support now requires 2.6.5 or above, in line with [Django 1.5's Python compatibility][python-compat]. + ## Deprecation policy We've now introduced an official deprecation policy, which is in line with [Django's deprecation policy][django-deprecation-policy]. This policy will make it easy for you to continue to track the latest, greatest version of REST framework. @@ -147,6 +149,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re [xordoquy]: https://github.com/xordoquy [django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ +[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy [credits]: http://django-rest-framework.org/topics/credits.html [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework From d05b950945ffe012de63f750ba5b98b14cfc4b9a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:16:49 +0000 Subject: [PATCH 058/128] Update release notes. --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3f3f87865..d5444f728 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,8 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Added TimeField. +* Serializer fields can be mapped to any method that takes no args, or only takes kwargs which have defaults. * Bugfix: request.DATA should return an empty `QueryDict` with no data, not `None`. * Bugfix: Remove unneeded field validation, which caused extra queries. From 50a9070e469dfc3c1018e9eee8ac9fe8c1a5f552 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:19:10 +0000 Subject: [PATCH 059/128] Added @pelme, for TimeField addition. Refs #660. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 1320d4d4b..bb41ef5f5 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -103,6 +103,7 @@ The following people have helped make REST framework great. * Fernando Rocha - [fernandogrd] * Xavier Ordoquy - [xordoquy] * Adam Wentz - [floppya] +* Andreas Pelme - [pelme] Many thanks to everyone who's contributed to the project. @@ -240,3 +241,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [fernandogrd]: https://github.com/fernandogrd [xordoquy]: https://github.com/xordoquy [floppya]: https://github.com/floppya +[pelme]: https://github.com/pelme From 618606888ab34418998d1abfe4668804038ff22f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Feb 2013 09:27:37 +0000 Subject: [PATCH 060/128] Mention caching. Closes #659. --- docs/api-guide/throttling.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 923593bcc..1abd49f47 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -6,8 +6,6 @@ > > [Twitter API rate limiting response][cite] -[cite]: https://dev.twitter.com/docs/error-codes-responses - Throttling is similar to [permissions], in that it determines if a request should be authorized. Throttles indicate a temporary state, and are used to control the rate of requests that clients can make to an API. As with permissions, multiple throttles may be used. Your API might have a restrictive throttle for unauthenticated requests, and a less restrictive throttle for authenticated requests. @@ -63,6 +61,10 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +## Setting up the cache + +The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details. + --- # API Reference @@ -162,4 +164,7 @@ The following is an example of a rate throttle, that will randomly throttle 1 in def allow_request(self, request, view): return random.randint(1, 10) == 1 +[cite]: https://dev.twitter.com/docs/error-codes-responses [permissions]: permissions.md +[cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches +[cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache \ No newline at end of file From d3f6536365cefa01f93cfadcc5e6a737d5c5fa80 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 15:33:36 +0600 Subject: [PATCH 061/128] Added tests for PATCH form in the Browsable API --- rest_framework/tests/renderers.py | 4 ++++ rest_framework/tests/utils.py | 16 ++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index e3f45ce60..90ef12212 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -112,6 +112,9 @@ class POSTDeniedView(APIView): def put(self, request): return Response() + def patch(self, request): + return Response() + class DocumentingRendererTests(TestCase): def test_only_permitted_forms_are_displayed(self): @@ -120,6 +123,7 @@ class DocumentingRendererTests(TestCase): response = view(request).render() self.assertNotContains(response, '>POST<') self.assertContains(response, '>PUT<') + self.assertContains(response, '>PATCH<') class RendererEndToEndTests(TestCase): diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index 224c4f9d3..8c87917d9 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals -from django.test.client import RequestFactory, FakePayload +from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory from django.test.client import MULTIPART_CONTENT from rest_framework.compat import urlparse -class RequestFactory(RequestFactory): +class RequestFactory(_RequestFactory): def __init__(self, **defaults): super(RequestFactory, self).__init__(**defaults) @@ -26,3 +26,15 @@ class RequestFactory(RequestFactory): } r.update(extra) return self.request(**r) + + +class Client(_Client, RequestFactory): + def patch(self, path, data={}, content_type=MULTIPART_CONTENT, + follow=False, **extra): + """ + Send a resource to the server using PATCH. + """ + response = super(Client, self).patch(path, data=data, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response From 3195f72784a2d55d10f3d7a58acdfee694e89e4b Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 16:39:24 +0600 Subject: [PATCH 062/128] POST form using new form.html template --- .../templates/rest_framework/base.html | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 87e5dc04e..fb541e944 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,25 +125,16 @@ {% if post_form %}
    -
    -
    - {% csrf_token %} - {{ post_form.non_field_errors }} - {% for field in post_form %} -
    - {{ field.label_tag|add_class:"control-label" }} -
    - {{ field }} - {{ field.help_text }} - -
    + {% with form=post_form %} + +
    + {% include "rest_framework/form.html" %} +
    +
    - {% endfor %} -
    - -
    -
    - +
    + + {% endwith %}
    {% endif %} From 533e47235210d735dbe68d96fb55460eca19be9b Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Fri, 15 Feb 2013 18:25:36 +0600 Subject: [PATCH 063/128] Added tabs between object form and generic content form on POST form --- .../templates/rest_framework/base.html | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index fb541e944..9d47a2edd 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,16 +125,40 @@ {% if post_form %}
    - {% with form=post_form %} -
    -
    - {% include "rest_framework/form.html" %} -
    - -
    -
    -
    - {% endwith %} + +
    + {% if post_form %} +
    + {% with form=post_form %} +
    +
    + {% include "rest_framework/form.html" %} +
    + +
    +
    +
    + {% endwith %} +
    + {% endif %} +
    + {% with form=generic_content_form %} +
    +
    + {% include "rest_framework/form.html" %} +
    + +
    +
    +
    + {% endwith %} +
    +
    {% endif %} From 818a2e8656962a63d2f3346036081c2f49362de5 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sun, 17 Feb 2013 20:02:12 -0800 Subject: [PATCH 064/128] One-to-many writable, nested serializer support --- rest_framework/serializers.py | 101 +++++++++++++++++++++++---- rest_framework/tests/nesting.py | 118 +++++++++++++++++++++++++++++++- 2 files changed, 203 insertions(+), 16 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 12cf614d9..8aa3e0225 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -108,19 +108,21 @@ class BaseSerializer(WritableField): _dict_class = SortedDictWithMetadata def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, many=None, source=None): + context=None, partial=False, many=None, source=None, required=True): super(BaseSerializer, self).__init__(source=source) self.opts = self._options_class(self.Meta) self.parent = None self.root = None self.partial = partial self.many = many + self.required = required self.context = context or {} self.init_data = data self.init_files = files self.object = instance + self.unused_objects = instance self.fields = self.get_fields() self._data = None @@ -280,13 +282,50 @@ class BaseSerializer(WritableField): ret.fields[key] = field return ret - def from_native(self, data, files): + def _get_object_from_data(self, data): + """ + Get the corresponding object to deserialize the data into. + """ + if self.unused_objects is None: return None + + # Just get the first object in the list. + if self.unused_objects: + obj = self.unused_objects[0] + obj = obj[0] + self.unused_objects.remove(obj) + else: + obj = None + + return obj + + def from_native(self, data, files, many=False): """ Deserialize primitives -> objects. """ - if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)): - # TODO: error data when deserializing lists - return [self.from_native(item, None) for item in data] + if many: + # Both our object and data must be in list form + if ((data is not None and not (hasattr(data, '__iter__') and not isinstance(data, (Page, dict)))) or + (self.object is not None and not (hasattr(self.object, '__iter__') and not isinstance(self.object, (Page, dict))))): + self._errors = {'non_field_errors': ['Invalid data']} + return None + + self._siblings = [] + self._errors = [] + for item in data: + obj = self._get_object_from_data(item) + # Deserializng a list of objects requires a separate + # serializer for each object (otherwise nested objects + # would share the nested serializers) + sibling = self.__class__(obj, data=item, many=False) + self._siblings.append(sibling) + sibling.object = sibling.from_native(item, None) + self._errors.append(sibling._errors) + for obj in self.unused_objects or []: + # Unused objects will be deleted. + sibling = self.__class__(obj) + sibling._delete = True + self._siblings.append(sibling) + return [sibling.object for sibling in self._siblings] self._errors = {} if data is not None or files is not None: @@ -353,10 +392,7 @@ class BaseSerializer(WritableField): 'Use the `many=True` flag when instantiating the serializer.', PendingDeprecationWarning, stacklevel=3) - # TODO: error data when deserializing lists - if many: - ret = [self.from_native(item, None) for item in data] - ret = self.from_native(data, files) + ret = self.from_native(data, files, many) if not self._errors: self.object = ret @@ -417,6 +453,27 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions + def _get_object_from_data(self, data): + """ + Get the corresponding object to deserialize the data into. + """ + if self.unused_objects is None: return None + + # Get the object based on pk + pk_field_name = self.opts.model._meta.pk.name + pk = data.get(pk_field_name, None) + if pk: + # Loop through objects and find one with pk or return None + obj = [o for o in self.unused_objects if o.pk == pk] + if obj: + obj = obj[0] + self.unused_objects.remove(obj) + else: + obj = None + return obj + else: + return None + def field_from_native(self, data, files, field_name, into): if self.read_only: return @@ -431,14 +488,18 @@ class ModelSerializer(Serializer): if self.parent.object: # Set the serializer object if it exists obj = getattr(self.parent.object, field_name) + if is_simple_callable(getattr(obj, 'all', None)): + # If this is a relational manager we just want the objects + obj = list(obj.all()) self.object = obj + self.unused_objects = obj if value in (None, ''): self._delete = True into[(self.source or field_name)] = self else: - obj = self.from_native(value, files) - if not self._errors: + obj = self.from_native(value, files, self.many) + if not any(self._errors): self.object = obj into[self.source or field_name] = self else: @@ -626,15 +687,25 @@ class ModelSerializer(Serializer): return instance - def from_native(self, data, files): + def from_native(self, data, files, many=False): """ Override the default method to also include model field validation. """ - instance = super(ModelSerializer, self).from_native(data, files) - if instance: - return self.full_clean(instance) + instance = super(ModelSerializer, self).from_native(data, files, many) + if self.many and hasattr(self, '_siblings'): + objects = [s.full_clean(s.object) if s.object else None for s in self._siblings] + self._errors = [s._errors for s in self._siblings] + return objects + else: + if instance: + return self.full_clean(instance) def _save(self, parent=None, fk_field=None): + if self.many: + for s in self._siblings: + s._save(parent, fk_field) + return + if self._delete: self.object.delete() return diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index f23ef5c18..a55ac9744 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -4,6 +4,28 @@ from django.test import TestCase from rest_framework import serializers +class OneToManyTarget(models.Model): + name = models.CharField(max_length=100) + + +class OneToManyTargetSource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(OneToManyTarget, + related_name='target_sources') + + +class OneToManySource(models.Model): + name = models.CharField(max_length=100) + target_source = models.ForeignKey(OneToManyTargetSource, + related_name='sources') + + +class OneToManySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(OneToManyTarget, + related_name='sources') + + class OneToOneTarget(models.Model): name = models.CharField(max_length=100) @@ -39,6 +61,101 @@ class OneToOneTargetSerializer(serializers.ModelSerializer): model = OneToOneTarget +class OneToManySourceSerializer(serializers.ModelSerializer): + class Meta: + model = OneToManySource + exclude = ('target_source', ) + + +class OneToManyTargetSourceSerializer(serializers.ModelSerializer): + sources = OneToManySourceSerializer(many=True, required=False) + + class Meta: + model = OneToManyTargetSource + exclude = ('target', ) + + +class OneToManyTargetSerializer(serializers.ModelSerializer): + target_sources = OneToManyTargetSourceSerializer(many=True) + + class Meta: + model = OneToManyTarget + + +class NestedOneToManyTests(TestCase): + def setUp(self): + target = OneToManyTarget(name='target-1') + target.save() + target_source = OneToManyTargetSource(name='target-source-1', target=target) + target_source.save() + for idx in range(1, 4): + source = OneToManySource(name='source-%d' % idx, target_source=target_source) + source.save() + + def test_one_to_many_retrieve(self): + queryset = OneToManyTarget.objects.all() + serializer = OneToManyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_sources': [{'id': 1, 'name': 'target-source-1', 'sources': [{'id': 1, 'name': 'source-1'}, {'id': 2, 'name': 'source-2'}, {'id': 3, 'name': 'source-3'}]}]} + ] + self.assertEquals(serializer.data, expected) + + def test_one_to_many_create(self): + # Note the nonsensical source id given + data = {'id': 2, 'name': 'target-2', 'target_sources': [{'id': 2, 'name': 'target-source-2', 'sources': [{'id': 2, 'name': 'source-4'}]}]} + expected = {'id': 2, 'name': 'target-2', 'target_sources': [{'id': 2, 'name': 'target-source-2', 'sources': [{'id': 4, 'name': 'source-4'}]}]} + serializer = OneToManyTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, expected) + self.assertEqual(obj.name, 'target-2') + + # Ensure (target 4, target_source 4, source 4) are added, and + # everything else is as expected. + queryset = OneToManyTarget.objects.all() + serializer = OneToManyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_sources': [{'id': 1, 'name': 'target-source-1', 'sources': [{'id': 1, 'name': 'source-1'}, {'id': 2, 'name': 'source-2'}, {'id': 3, 'name': 'source-3'}]}]}, + {'id': 2, 'name': 'target-2', 'target_sources': [{'id': 2, 'name': 'target-source-2', 'sources': [{'id': 4, 'name': 'source-4'}]}]} + ] + self.assertEquals(serializer.data, expected) + + def test_one_to_many_update(self): + data = {'id': 1, 'name': 'target-1-updated', 'target_sources': [{'id': 1, 'name': 'target-source-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, {'id': 2, 'name': 'source-2'}, {'id': 3, 'name': 'source-3'}]}]} + expected = {'id': 1, 'name': 'target-1-updated', 'target_sources': [{'id': 1, 'name': 'target-source-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, {'id': 2, 'name': 'source-2'}, {'id': 3, 'name': 'source-3'}]}]} + instance = OneToManyTarget.objects.get(pk=1) + serializer = OneToManyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, expected) + self.assertEqual(obj.name, 'target-1-updated') + + # Ensure (target 1, target_source 1, source 1) are updated, + # and everything else is as expected. + queryset = OneToManyTarget.objects.all() + serializer = OneToManyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1-updated', 'target_sources': [{'id': 1, 'name': 'target-source-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'}, {'id': 2, 'name': 'source-2'}, {'id': 3, 'name': 'source-3'}]}]} + ] + self.assertEquals(serializer.data, expected) + + def test_one_to_many_delete(self): + data = {'id': 1, 'name': 'target-1', 'target_sources': [{'id': 1, 'name': 'target-source-1', 'sources': [{'id': 2, 'name': 'source-2'}]}]} + instance = OneToManyTarget.objects.get(pk=1) + serializer = OneToManyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + + # Ensure target_source 1 is deleted, and everything else is as + # expected. + queryset = OneToManyTarget.objects.all() + serializer = OneToManyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': 'target-1', 'target_sources': [{'id': 1, 'name': 'target-source-1', 'sources': [{'id': 2, 'name': 'source-2'}]}]} + ] + self.assertEquals(serializer.data, expected) + + class NestedOneToOneTests(TestCase): def setUp(self): for idx in range(1, 4): @@ -58,7 +175,6 @@ class NestedOneToOneTests(TestCase): {'id': 3, 'name': 'target-3', 'target_source': {'id': 3, 'name': 'target-source-3', 'source': {'id': 3, 'name': 'source-3'}}} ] self.assertEquals(serializer.data, expected) - def test_one_to_one_create(self): data = {'id': 4, 'name': 'target-4', 'target_source': {'id': 4, 'name': 'target-source-4', 'source': {'id': 4, 'name': 'source-4'}}} From 66a6ffaf957405691d0714fc422b46a6927639a7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Feb 2013 17:09:28 +0000 Subject: [PATCH 065/128] Fix typos. --- docs/api-guide/relations.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 25fca4753..5a9d74b09 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -43,7 +43,7 @@ In order to explain the various types of relational fields, we'll use a couple o For example, the following serializer. - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = RelatedField(many=True) class Meta: @@ -75,7 +75,7 @@ This field is read only. For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = PrimaryKeyRelatedField(many=True, read_only=True) class Meta: @@ -109,7 +109,7 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = HyperlinkedRelatedField(many=True, read_only=True, view_name='track-detail') @@ -149,7 +149,7 @@ By default this field is read-write, although you can change this behavior using For example, the following serializer: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = SlugRelatedField(many=True, read_only=True, slug_field='title') class Meta: @@ -223,12 +223,12 @@ Note that nested relationships are currently read-only. For read-write relation For example, the following serializer: - class TrackSerializer(serializer.ModelSerializer): + class TrackSerializer(serializers.ModelSerializer): class Meta: model = Track fields = ('order', 'title') - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = TrackSerializer(many=True) class Meta: @@ -265,7 +265,7 @@ For, example, we could define a relational field, to serialize a track to a cust duration = time.strftime('%M:%S', time.gmtime(value.duration)) return 'Track %d: %s (%s)' % (value.order, value.name, duration) - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = TrackListingField(many=True) class Meta: @@ -295,13 +295,13 @@ Note that reverse relationships are not automatically generated by the `ModelSer **The following will not work:** - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): class Meta: fields = ('tracks', ...) Instead, you must explicitly add it to the serializer. For example: - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = serializers.PrimaryKeyRelationship(many=True) ... @@ -315,7 +315,7 @@ The best way to ensure this is typically to make sure that the relationship on t Alternatively, you can use the `source` argument on the serializer field, to use a different accessor attribute than the field name. For example. - class AlbumSerializer(serializer.ModelSerializer): + class AlbumSerializer(serializers.ModelSerializer): tracks = serializers.PrimaryKeyRelationship(many=True, source='track_set') See the Django documentation on [reverse relationships][reverse-relationships] for more details. From c5cf51cf511c84ab3e446376ff38170dcd421958 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Feb 2013 17:16:48 +0000 Subject: [PATCH 066/128] Fix typos. --- docs/api-guide/relations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 5a9d74b09..623fe1a90 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -302,7 +302,7 @@ Note that reverse relationships are not automatically generated by the `ModelSer Instead, you must explicitly add it to the serializer. For example: class AlbumSerializer(serializers.ModelSerializer): - tracks = serializers.PrimaryKeyRelationship(many=True) + tracks = serializers.PrimaryKeyRelatedField(many=True) ... By default, the field will uses the same accessor as it's field name to retrieve the relationship, so in this example, `Album` instances would need to have the `tracks` attribute for this relationship to work. @@ -316,7 +316,7 @@ The best way to ensure this is typically to make sure that the relationship on t Alternatively, you can use the `source` argument on the serializer field, to use a different accessor attribute than the field name. For example. class AlbumSerializer(serializers.ModelSerializer): - tracks = serializers.PrimaryKeyRelationship(many=True, source='track_set') + tracks = serializers.PrimaryKeyRelatedField(many=True, source='track_set') See the Django documentation on [reverse relationships][reverse-relationships] for more details. From 160d10d348bc44cb481d30592253ef7832210f4b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Feb 2013 08:46:00 +0000 Subject: [PATCH 067/128] Fix docstring --- rest_framework/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 1e481c874..fa7425828 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -211,13 +211,13 @@ class APIView(View): def get_parsers(self): """ - Instantiates and returns the list of renderers that this view can use. + Instantiates and returns the list of parsers that this view can use. """ return [parser() for parser in self.parser_classes] def get_authenticators(self): """ - Instantiates and returns the list of renderers that this view can use. + Instantiates and returns the list of authenticators that this view can use. """ return [auth() for auth in self.authentication_classes] From fc5f982ccc761efd5a6ee320dad7b97ebf9cfad8 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 20 Feb 2013 11:12:54 +0200 Subject: [PATCH 068/128] =?UTF-8?q?Don=E2=80=99t=20use=20my=20old=20nickna?= =?UTF-8?q?me=20in=20credits.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/topics/credits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bb41ef5f5..990f3cb6e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -4,7 +4,7 @@ The following people have helped make REST framework great. * Tom Christie - [tomchristie] * Marko Tibold - [markotibold] -* Paul Bagwell - [pbgwl] +* Paul Miller - [paulmillr] * Sébastien Piquemal - [sebpiq] * Carmen Wick - [cwick] * Alex Ehlke - [aehlke] From 47a4f0863d08e4b839ea3bbd7308ecc0f995b7d9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Feb 2013 09:18:54 +0000 Subject: [PATCH 069/128] Update link to @paulmillr. Refs #668. --- docs/topics/credits.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 990f3cb6e..e546548e6 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -142,7 +142,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [tomchristie]: https://github.com/tomchristie [markotibold]: https://github.com/markotibold -[pbgwl]: https://github.com/pbgwl +[paulmillr]: https://github.com/paulmillr [sebpiq]: https://github.com/sebpiq [cwick]: https://github.com/cwick [aehlke]: https://github.com/aehlke From 2fb6fa2dd3b336cc442e707dbb80a4d5616582a6 Mon Sep 17 00:00:00 2001 From: Michael Elovskikh Date: Wed, 20 Feb 2013 17:15:12 +0600 Subject: [PATCH 070/128] Minimal forms appearance improvements --- rest_framework/static/rest_framework/css/default.css | 11 +++++++++++ rest_framework/static/rest_framework/js/default.js | 2 +- rest_framework/templates/rest_framework/base.html | 12 ++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index b2e41b994..731075271 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -150,6 +150,17 @@ html, body { margin: 0 auto -60px; } +.form-switcher { + margin-bottom: 0; +} + +.tab-content { + padding-top: 25px; + background: #fff; + border: 1px solid #ddd; + border-top: none; + border-radius: 0 0 4px 4px; +} #footer, #push { height: 60px; /* .push must be the same height as .footer */ diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index bc5b02928..484a3bdf1 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -4,4 +4,4 @@ $('.js-tooltip').tooltip({ delay: 1000 }); -$('#form-switcher a:first').tab('show'); \ No newline at end of file +$('.form-switcher a:first').tab('show'); \ No newline at end of file diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 9d47a2edd..2fe7b6536 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -125,11 +125,11 @@ {% if post_form %}
    -