From e029e44477111ed16da8954a53dcf08c08cfb403 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Sat, 10 Nov 2012 16:09:14 -0800 Subject: [PATCH 01/79] Added authtoken login/logout urlpatterns and views to support scripted logins and logouts using TokenAuthentication. Added unittests. --- rest_framework/authtoken/serializers.py | 37 +++++++++++++++++++++++++ rest_framework/authtoken/urls.py | 21 ++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 rest_framework/authtoken/serializers.py create mode 100644 rest_framework/authtoken/urls.py diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py new file mode 100644 index 000000000..8e0128c14 --- /dev/null +++ b/rest_framework/authtoken/serializers.py @@ -0,0 +1,37 @@ +from django.contrib.auth import authenticate + +from rest_framework import serializers +from rest_framework.authtoken.models import Token + + +class AuthTokenSerializer(serializers.Serializer): + token = serializers.Field(source="key") + username = serializers.CharField(max_length=30) + password = serializers.CharField() + + def validate(self, attrs): + username = attrs.get('username') + password = attrs.get('password') + + if username and password: + user = authenticate(username=username, password=password) + + if user: + if not user.is_active: + raise serializers.ValidationError('User account is disabled.') + attrs['user'] = user + return attrs + else: + raise serializers.ValidationError('Unable to login with provided credentials.') + else: + raise serializers.ValidationError('Must include "username" and "password"') + + def convert_object(self, obj): + ret = self._dict_class() + ret['token'] = obj.key + ret['user'] = obj.user.id + return ret + + def restore_object(self, attrs, instance=None): + token, created = Token.objects.get_or_create(user=attrs['user']) + return token diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py new file mode 100644 index 000000000..2a3e81150 --- /dev/null +++ b/rest_framework/authtoken/urls.py @@ -0,0 +1,21 @@ +""" +Login and logout views for token authentication. + +Add these to your root URLconf if you're using token authentication +your API requires authentication. + +The urls must be namespaced as 'rest_framework', and you should make sure +your authentication settings include `TokenAuthentication`. + + urlpatterns = patterns('', + ... + url(r'^auth-token', include('rest_framework.authtoken.urls', namespace='rest_framework')) + ) +""" +from django.conf.urls.defaults import patterns, url +from rest_framework.authtoken.views import AuthTokenView + +urlpatterns = patterns('rest_framework.authtoken.views', + url(r'^login/$', AuthTokenView.as_view(), name='token_login'), +# url(r'^logout/$', 'token_logout', name='token_logout'), +) From 063c027c7b4278620fda6cf4aa7825a817748cc0 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Sat, 10 Nov 2012 16:17:50 -0800 Subject: [PATCH 02/79] Added authtoken login/logout urlpatterns and views --- rest_framework/authtoken/urls.py | 6 ++-- rest_framework/authtoken/views.py | 19 +++++++++++ rest_framework/tests/authentication.py | 46 +++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 2a3e81150..8bea46c00 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -13,9 +13,9 @@ your authentication settings include `TokenAuthentication`. ) """ from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenView +from rest_framework.authtoken.views import AuthTokenLoginView, AuthTokenLogoutView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenView.as_view(), name='token_login'), -# url(r'^logout/$', 'token_logout', name='token_logout'), + url(r'^login/$', AuthTokenLoginView.as_view(), name='token_login'), + url(r'^logout/$', AuthTokenLogoutView.as_view(), name='token_logout'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index e69de29bb..a52f0a77c 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -0,0 +1,19 @@ +from rest_framework.views import APIView +from rest_framework.generics import CreateAPIView +from rest_framework.authtoken.models import Token +from rest_framework.authtoken.serializers import AuthTokenSerializer +from django.http import HttpResponse + +class AuthTokenLoginView(CreateAPIView): + model = Token + serializer_class = AuthTokenSerializer + + +class AuthTokenLogoutView(APIView): + def post(self, request): + if request.user.is_authenticated() and request.auth: + request.auth.delete() + return HttpResponse("logged out") + else: + return HttpResponse("not logged in") + diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 8ab4c4e40..d1bc23d9b 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import patterns, include from django.contrib.auth.models import User from django.test import Client, TestCase @@ -27,6 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,) urlpatterns = patterns('', (r'^$', MockView.as_view()), + (r'^auth-token/', include('rest_framework.authtoken.urls')), ) @@ -152,3 +153,46 @@ class TokenAuthTests(TestCase): self.token.delete() token = Token.objects.create(user=self.user) self.assertTrue(bool(token.key)) + + def test_token_login_json(self): + """Ensure token login view using JSON POST works.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username, 'password': self.password}), 'application/json') + self.assertEqual(response.status_code, 201) + self.assertEqual(json.loads(response.content)['token'], self.key) + + def test_token_login_json_bad_creds(self): + """Ensure token login view using JSON POST fails if bad credentials are used.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + self.assertEqual(response.status_code, 400) + + def test_token_login_json_missing_fields(self): + """Ensure token login view using JSON POST fails if missing fields.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username}), 'application/json') + self.assertEqual(response.status_code, 400) + + def test_token_login_form(self): + """Ensure token login view using form POST works.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + {'username': self.username, 'password': self.password}) + self.assertEqual(response.status_code, 201) + self.assertEqual(json.loads(response.content)['token'], self.key) + + def test_token_logout(self): + """Ensure token logout view using JSON POST works.""" + # Use different User and Token as to isolate this test's effects on other unittests in class + username = "ringo" + user = User.objects.create_user(username, "starr@thebeatles.com", "pass") + token = Token.objects.create(user=user) + auth = "Token " + token.key + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/logout/', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + # Ensure token no longer exists + self.assertRaises(Token.DoesNotExist, lambda token: Token.objects.get(key=token.key), token) From 8eb37e1f7e879fc53c4550e5f1a545dd755cd07e Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Mon, 12 Nov 2012 15:16:53 -0800 Subject: [PATCH 03/79] Updates to login view for TokenAuthentication from feedback from Tom --- rest_framework/authtoken/serializers.py | 15 +------------- rest_framework/authtoken/urls.py | 5 ++--- rest_framework/authtoken/views.py | 27 ++++++++++++++----------- rest_framework/tests/authentication.py | 25 ++++++----------------- 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 8e0128c14..a5ed6e6d7 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,12 +1,8 @@ from django.contrib.auth import authenticate - from rest_framework import serializers -from rest_framework.authtoken.models import Token - class AuthTokenSerializer(serializers.Serializer): - token = serializers.Field(source="key") - username = serializers.CharField(max_length=30) + username = serializers.CharField() password = serializers.CharField() def validate(self, attrs): @@ -26,12 +22,3 @@ class AuthTokenSerializer(serializers.Serializer): else: raise serializers.ValidationError('Must include "username" and "password"') - def convert_object(self, obj): - ret = self._dict_class() - ret['token'] = obj.key - ret['user'] = obj.user.id - return ret - - def restore_object(self, attrs, instance=None): - token, created = Token.objects.get_or_create(user=attrs['user']) - return token diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 8bea46c00..878721367 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -13,9 +13,8 @@ your authentication settings include `TokenAuthentication`. ) """ from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenLoginView, AuthTokenLogoutView +from rest_framework.authtoken.views import AuthTokenView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenLoginView.as_view(), name='token_login'), - url(r'^logout/$', AuthTokenLogoutView.as_view(), name='token_logout'), + url(r'^login/$', AuthTokenView.as_view(), name='token_login'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index a52f0a77c..e027dff1c 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -1,19 +1,22 @@ from rest_framework.views import APIView -from rest_framework.generics import CreateAPIView +from rest_framework import status +from rest_framework import parsers +from rest_framework import renderers +from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -from django.http import HttpResponse -class AuthTokenLoginView(CreateAPIView): +class AuthTokenView(APIView): + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) model = Token - serializer_class = AuthTokenSerializer - -class AuthTokenLogoutView(APIView): def post(self, request): - if request.user.is_authenticated() and request.auth: - request.auth.delete() - return HttpResponse("logged out") - else: - return HttpResponse("not logged in") - + serializer = AuthTokenSerializer(data=request.DATA) + if serializer.is_valid(): + token, created = Token.objects.get_or_create(user=serializer.object['user']) + return Response({'token': token.key}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index d1bc23d9b..cb16ef1e8 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -158,41 +158,28 @@ class TokenAuthTests(TestCase): """Ensure token login view using JSON POST works.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username, 'password': self.password}), 'application/json') - self.assertEqual(response.status_code, 201) + json.dumps({'username': self.username, 'password': self.password}), 'application/json') + self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content)['token'], self.key) def test_token_login_json_bad_creds(self): """Ensure token login view using JSON POST fails if bad credentials are used.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_json_missing_fields(self): """Ensure token login view using JSON POST fails if missing fields.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username}), 'application/json') + json.dumps({'username': self.username}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_form(self): """Ensure token login view using form POST works.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - {'username': self.username, 'password': self.password}) - self.assertEqual(response.status_code, 201) - self.assertEqual(json.loads(response.content)['token'], self.key) - - def test_token_logout(self): - """Ensure token logout view using JSON POST works.""" - # Use different User and Token as to isolate this test's effects on other unittests in class - username = "ringo" - user = User.objects.create_user(username, "starr@thebeatles.com", "pass") - token = Token.objects.create(user=user) - auth = "Token " + token.key - client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/logout/', HTTP_AUTHORIZATION=auth) + {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, 200) - # Ensure token no longer exists - self.assertRaises(Token.DoesNotExist, lambda token: Token.objects.get(key=token.key), token) + self.assertEqual(json.loads(response.content)['token'], self.key) From cc55a7b64310cdd4b8b96e8270a48fd994ede90c Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 18:00:41 +0100 Subject: [PATCH 04/79] Returning a Location Header on Create when creating a Resource with HyperlinkedIdentityField of any name --- rest_framework/mixins.py | 19 +++++++++++++++++-- .../tests/hyperlinkedserializers.py | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88e..f54b5b1f6 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,6 +7,7 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from rest_framework.fields import HyperlinkedIdentityField class CreateModelMixin(object): @@ -19,9 +20,23 @@ class CreateModelMixin(object): if serializer.is_valid(): self.pre_save(serializer.object) self.object = serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + headers = self.get_success_headers(serializer) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + + def get_success_headers(self,serializer): + headers = [] + identity_field = identity_name = None + for name,field in serializer.fields.iteritems(): + if isinstance(field,HyperlinkedIdentityField): + identity_name, identity_field = name, field + if identity_field: + #identity_field.initialize(serializer,"url") + headers.append( + ("Location",identity_field.field_to_native(self.object,identity_name)) + ) + return headers + def pre_save(self, obj): pass diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 5ab850afa..cc5a19c16 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -8,6 +8,7 @@ factory = RequestFactory() class BlogPostCommentSerializer(serializers.ModelSerializer): + custom_identity_field = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail') text = serializers.CharField() blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail') @@ -17,6 +18,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer): class PhotoSerializer(serializers.Serializer): + """ When Adding a HyperlinkedIdentityField to this serializer, the TestCreateWithForeignKeysAndCustomSlug will fail """ description = serializers.CharField() album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title') @@ -53,6 +55,9 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView): model = BlogPostComment serializer_class = BlogPostCommentSerializer +class BlogPostCommentDetail(generics.RetrieveAPIView): + model = BlogPostComment + serializer_class = BlogPostCommentSerializer class BlogPostDetail(generics.RetrieveAPIView): model = BlogPost @@ -80,6 +85,7 @@ urlpatterns = patterns('', url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), url(r'^posts/(?P\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'), url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'), + url(r'^comments/(?P\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'), url(r'^albums/(?P\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'), url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'), url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'), @@ -191,6 +197,7 @@ class TestCreateWithForeignKeys(TestCase): request = factory.post('/comments/', data=data) response = self.create_view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response["Location"], 'http://testserver/comments/1/') self.assertEqual(self.post.blogpostcomment_set.count(), 1) self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') @@ -215,6 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase): request = factory.post('/photos/', data=data) response = self.list_create_view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertNotIn("Location",response,msg="A Serializer without HyperlinkedIdentityField can not produce a valid Location header (for now). Thats why there shouldn'd be one") self.assertEqual(self.post.photo_set.count(), 1) self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo') From 573de11b233a85347456a4d7e50fd7345d13db03 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Tue, 13 Nov 2012 18:07:38 +0100 Subject: [PATCH 05/79] changed buggy response + code ploishing reponse didnt handle any headers at all. Accepts now a dict of headers and sets those properly --- rest_framework/mixins.py | 11 +++-------- rest_framework/response.py | 5 ++++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f54b5b1f6..eddd8f49e 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -25,16 +25,11 @@ class CreateModelMixin(object): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def get_success_headers(self,serializer): - headers = [] - identity_field = identity_name = None + headers = {} for name,field in serializer.fields.iteritems(): if isinstance(field,HyperlinkedIdentityField): - identity_name, identity_field = name, field - if identity_field: - #identity_field.initialize(serializer,"url") - headers.append( - ("Location",identity_field.field_to_native(self.object,identity_name)) - ) + headers["Location"] = field.field_to_native(self.object,name) + break return headers def pre_save(self, obj): diff --git a/rest_framework/response.py b/rest_framework/response.py index 0de01204d..88f0019fc 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -20,9 +20,12 @@ class Response(SimpleTemplateResponse): """ super(Response, self).__init__(None, status=status) self.data = data - self.headers = headers and headers[:] or [] self.template_name = template_name self.exception = exception + + if headers: + for name,value in headers.iteritems(): + self[name] = value @property def rendered_content(self): From 851dff1644f6dafd29838a997a788df51c0570a3 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Tue, 13 Nov 2012 18:39:07 +0100 Subject: [PATCH 06/79] fixed a bug on testing throttling headers after changing the headers storing of reponse --- rest_framework/tests/throttling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/throttling.py b/rest_framework/tests/throttling.py index 0b94c25ba..4b98b9414 100644 --- a/rest_framework/tests/throttling.py +++ b/rest_framework/tests/throttling.py @@ -106,7 +106,7 @@ class ThrottlingTests(TestCase): if expect is not None: self.assertEquals(response['X-Throttle-Wait-Seconds'], expect) else: - self.assertFalse('X-Throttle-Wait-Seconds' in response.headers) + self.assertFalse('X-Throttle-Wait-Seconds' in response) def test_seconds_fields(self): """ From b341dc70af828d066eb3891e8eafb6337cdd2d04 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Tue, 13 Nov 2012 19:15:42 +0100 Subject: [PATCH 07/79] fixed ugly code Location header is set just, if there is a Location field on the serializer. --- rest_framework/mixins.py | 14 ++++++-------- rest_framework/tests/hyperlinkedserializers.py | 7 +++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index eddd8f49e..832365e56 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -20,17 +20,15 @@ class CreateModelMixin(object): if serializer.is_valid(): self.pre_save(serializer.object) self.object = serializer.save() - headers = self.get_success_headers(serializer) + headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def get_success_headers(self,serializer): - headers = {} - for name,field in serializer.fields.iteritems(): - if isinstance(field,HyperlinkedIdentityField): - headers["Location"] = field.field_to_native(self.object,name) - break - return headers + def get_success_headers(self,data): + if "url" in data: + return {'Location':data.get("url")} + else: + return {} def pre_save(self, obj): pass diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index cc5a19c16..5fc935ae4 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -8,17 +8,16 @@ factory = RequestFactory() class BlogPostCommentSerializer(serializers.ModelSerializer): - custom_identity_field = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail') + url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail') text = serializers.CharField() blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail') class Meta: model = BlogPostComment - fields = ('text', 'blog_post_url') + fields = ('text', 'blog_post_url', 'url') class PhotoSerializer(serializers.Serializer): - """ When Adding a HyperlinkedIdentityField to this serializer, the TestCreateWithForeignKeysAndCustomSlug will fail """ description = serializers.CharField() album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title') @@ -222,7 +221,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase): request = factory.post('/photos/', data=data) response = self.list_create_view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertNotIn("Location",response,msg="A Serializer without HyperlinkedIdentityField can not produce a valid Location header (for now). Thats why there shouldn'd be one") + self.assertNotIn("Location", response, msg="Location should only be included if there is a 'url' field on the serializer") self.assertEqual(self.post.photo_set.count(), 1) self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo') From 3a30a9b1cbb4444adf3cbb1d3d80c637b5f4f2ca Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Tue, 13 Nov 2012 20:30:18 +0100 Subject: [PATCH 08/79] removed useless line after polishing code added it in first commit but after third it became useless. --- rest_framework/mixins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 832365e56..f6119aa18 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,7 +7,6 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response -from rest_framework.fields import HyperlinkedIdentityField class CreateModelMixin(object): From 5443dd5f3c5f75cd1524eb26c6d5b53df3594f9b Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Tue, 13 Nov 2012 23:26:17 +0100 Subject: [PATCH 09/79] Added a FileField and an ImageField (copied from django.forms.fields). Adjusted generics, mixins and serializers to take a `files` arg where applicable. --- rest_framework/fields.py | 91 +++++++++++++++++++++++++++++++++++ rest_framework/generics.py | 3 +- rest_framework/mixins.py | 4 +- rest_framework/serializers.py | 21 +++++--- 4 files changed, 108 insertions(+), 11 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c2064261..9cd84c0d3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -904,3 +904,94 @@ class FloatField(WritableField): except (TypeError, ValueError): msg = self.error_messages['invalid'] % value raise ValidationError(msg) + + +class FileField(WritableField): + type_name = 'FileField' + + default_error_messages = { + 'invalid': _("No file was submitted. Check the encoding type on the form."), + 'missing': _("No file was submitted."), + 'empty': _("The submitted file is empty."), + 'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'), + 'contradiction': _('Please either submit a file or check the clear checkbox, not both.') + } + + def __init__(self, *args, **kwargs): + self.max_length = kwargs.pop('max_length', None) + self.allow_empty_file = kwargs.pop('allow_empty_file', False) + super(FileField, self).__init__(*args, **kwargs) + + def from_native(self, data): + if data in validators.EMPTY_VALUES: + return None + + # UploadedFile objects should have name and size attributes. + try: + file_name = data.name + file_size = data.size + except AttributeError: + raise ValidationError(self.error_messages['invalid']) + + if self.max_length is not None and len(file_name) > self.max_length: + error_values = {'max': self.max_length, 'length': len(file_name)} + raise ValidationError(self.error_messages['max_length'] % error_values) + if not file_name: + raise ValidationError(self.error_messages['invalid']) + if not self.allow_empty_file and not file_size: + raise ValidationError(self.error_messages['empty']) + + return data + + def to_native(self, value): + """ + No need to return anything, the file can be accessed form its url. + """ + return + + +class ImageField(FileField): + default_error_messages = { + 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), + } + + def from_native(self, data): + """ + Checks that the file-upload field data contains a valid image (GIF, JPG, + PNG, possibly others -- whatever the Python Imaging Library supports). + """ + f = super(ImageField, self).from_native(data) + if f is None: + return None + + # Try to import PIL in either of the two ways it can end up installed. + try: + from PIL import Image + except ImportError: + import Image + + # We need to get a file object for PIL. We might have a path or we might + # have to read the data into memory. + if hasattr(data, 'temporary_file_path'): + file = data.temporary_file_path() + else: + if hasattr(data, 'read'): + file = BytesIO(data.read()) + else: + file = BytesIO(data['content']) + + try: + # load() could spot a truncated JPEG, but it loads the entire + # image in memory, which is a DoS vector. See #3848 and #18520. + # verify() must be called immediately after the constructor. + Image.open(file).verify() + except ImportError: + # Under PyPy, it is possible to import PIL. However, the underlying + # _imaging C module isn't available, so an ImportError will be + # raised. Catch and re-raise. + raise + except Exception: # Python Imaging Library doesn't recognize it as an image + raise ValidationError(self.error_messages['invalid_image']) + if hasattr(f, 'seek') and callable(f.seek): + f.seek(0) + return f diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ebd06e452..d47c39cda 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -44,11 +44,10 @@ class GenericAPIView(views.APIView): return serializer_class def get_serializer(self, instance=None, data=None, files=None): - # TODO: add support for files # TODO: add support for seperate serializer/deserializer serializer_class = self.get_serializer_class() context = self.get_serializer_context() - return serializer_class(instance, data=data, context=context) + return serializer_class(instance, data=data, files=files, context=context) class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88e..991f4c500 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -15,7 +15,7 @@ class CreateModelMixin(object): Should be mixed in with any `BaseView`. """ def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.DATA) + serializer = self.get_serializer(data=request.DATA, files=request.FILES) if serializer.is_valid(): self.pre_save(serializer.object) self.object = serializer.save() @@ -80,7 +80,7 @@ class UpdateModelMixin(object): self.object = None success_status = status.HTTP_201_CREATED - serializer = self.get_serializer(self.object, data=request.DATA) + serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES) if serializer.is_valid(): self.pre_save(serializer.object) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46d4765e1..a46432a9c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -91,7 +91,7 @@ class BaseSerializer(Field): _options_class = SerializerOptions _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations. - def __init__(self, instance=None, data=None, context=None, **kwargs): + def __init__(self, instance=None, data=None, files=None, context=None, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.fields = copy.deepcopy(self.base_fields) @@ -101,9 +101,11 @@ class BaseSerializer(Field): self.context = context or {} self.init_data = data + self.init_files = files self.object = instance self._data = None + self._files = None self._errors = None ##### @@ -187,7 +189,7 @@ class BaseSerializer(Field): ret.fields[key] = field return ret - def restore_fields(self, data): + def restore_fields(self, data, files): """ Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. @@ -196,7 +198,10 @@ class BaseSerializer(Field): reverted_data = {} for field_name, field in fields.items(): try: - field.field_from_native(data, field_name, reverted_data) + if isinstance(field, (FileField, ImageField)): + field.field_from_native(files, field_name, reverted_data) + else: + field.field_from_native(data, field_name, reverted_data) except ValidationError as err: self._errors[field_name] = list(err.messages) @@ -250,7 +255,7 @@ class BaseSerializer(Field): return [self.convert_object(item) for item in obj] return self.convert_object(obj) - def from_native(self, data): + def from_native(self, data, files): """ Deserialize primatives -> objects. """ @@ -259,8 +264,8 @@ class BaseSerializer(Field): return (self.from_native(item) for item in data) self._errors = {} - if data is not None: - attrs = self.restore_fields(data) + if data is not None or files is not None: + attrs = self.restore_fields(data, files) attrs = self.perform_validation(attrs) else: self._errors['non_field_errors'] = ['No input provided'] @@ -288,7 +293,7 @@ class BaseSerializer(Field): setting self.object if no errors occurred. """ if self._errors is None: - obj = self.from_native(self.init_data) + obj = self.from_native(self.init_data, self.init_files) if not self._errors: self.object = obj return self._errors @@ -440,6 +445,8 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, + models.FileField: FileField, + models.ImageField: ImageField, } try: return field_mapping[model_field.__class__](**kwargs) From d3ee5080a0ff3894050442146083f9d4a2327c8f Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 15:03:42 -0800 Subject: [PATCH 10/79] Added documentation on how to use the token authentication login view. --- docs/api-guide/authentication.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 3137b9d4c..50d8c0546 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -97,6 +97,22 @@ If successfully authenticated, `TokenAuthentication` provides the following cred **Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only. +When using TokenAuthentication, it may be useful to add a login view for clients to retrieve the token. + +REST framework provides a built-in login view. To use it, add a pattern to include the token login view for clients as follows: + + urlpatterns += patterns('', + url(r'^api-token-auth/', include('rest_framework.authtoken.urls', + namespace='rest_framework')) + ) + +The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. + +The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: + + { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } + + ## OAuthAuthentication This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf. From 8cdbc0a33a69f0a170e92be47189f6006c147137 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Wed, 14 Nov 2012 00:09:39 +0100 Subject: [PATCH 11/79] Properly render file inputs in the Browsable api. --- rest_framework/fields.py | 2 +- rest_framework/renderers.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9cd84c0d3..162d22714 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -908,7 +908,7 @@ class FloatField(WritableField): class FileField(WritableField): type_name = 'FileField' - + widget = widgets.FileInput default_error_messages = { 'invalid': _("No file was submitted. Check the encoding type on the form."), 'missing': _("No file was submitted."), diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 22fd6e740..dab973467 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -320,7 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.SlugRelatedField: forms.ChoiceField, serializers.ManySlugRelatedField: forms.MultipleChoiceField, serializers.HyperlinkedRelatedField: forms.ChoiceField, - serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField + serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField, + serializers.FileField: forms.FileField, + serializers.ImageField: forms.ImageField, } fields = {} From 4fd590f96f77eae433f1d5de281ed95f5a003745 Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 16:49:13 -0800 Subject: [PATCH 12/79] Renamed AuthTokenView to ObtainAuthToken, added obtain_auth_token var, updated tests & docs. Left authtoken.urls in place as example. --- docs/api-guide/authentication.md | 14 ++++---------- docs/topics/credits.md | 2 ++ docs/topics/release-notes.md | 1 + rest_framework/authtoken/urls.py | 14 +++++++------- rest_framework/authtoken/views.py | 4 +++- rest_framework/tests/authentication.py | 2 +- 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 50d8c0546..18620f492 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -97,21 +97,15 @@ If successfully authenticated, `TokenAuthentication` provides the following cred **Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only. -When using TokenAuthentication, it may be useful to add a login view for clients to retrieve the token. - -REST framework provides a built-in login view. To use it, add a pattern to include the token login view for clients as follows: +REST framework provides a built-in login view for clients to retrieve the token called `rest_framework.authtoken.obtain_auth_token`. To use it, add a pattern to include the token login view for clients as follows: urlpatterns += patterns('', - url(r'^api-token-auth/', include('rest_framework.authtoken.urls', - namespace='rest_framework')) + url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token') ) -The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. - -The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: - - { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } +The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: + { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } ## OAuthAuthentication diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 22d08df7a..f378a5216 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -59,6 +59,7 @@ The following people have helped make REST framework great. * Toni Michel - [tonimichel] * Ben Konrath - [benkonrath] * Marc Aymerich - [glic3rinu] +* Rob Romano - [robromano] Many thanks to everyone who's contributed to the project. @@ -153,3 +154,4 @@ To contact the author directly: [tonimichel]: https://github.com/tonimichel [benkonrath]: https://github.com/benkonrath [glic3rinu]: https://github.com/glic3rinu +[robromano]: https://github.com/robromano diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 35e8a8b35..daacc76f5 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. +* Add convenience login view to get tokens when using `TokenAuthentication` ## 2.1.2 diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 878721367..a3419da60 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -1,20 +1,20 @@ """ -Login and logout views for token authentication. +Login view for token authentication. -Add these to your root URLconf if you're using token authentication +Add this to your root URLconf if you're using token authentication your API requires authentication. -The urls must be namespaced as 'rest_framework', and you should make sure -your authentication settings include `TokenAuthentication`. +You should make sure your authentication settings include +`TokenAuthentication`. urlpatterns = patterns('', ... - url(r'^auth-token', include('rest_framework.authtoken.urls', namespace='rest_framework')) + url(r'^auth-token/', 'rest_framework.authtoken.obtain_auth_token') ) """ + from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenView.as_view(), name='token_login'), + url(r'^login/$', 'rest_framework.authtoken.views.obtain_auth_token', name='token_login'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index e027dff1c..3ac674e28 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -class AuthTokenView(APIView): +class ObtainAuthToken(APIView): throttle_classes = () permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) @@ -20,3 +20,5 @@ class AuthTokenView(APIView): return Response({'token': token.key}) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +obtain_auth_token = ObtainAuthToken.as_view() diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index cb16ef1e8..96ca9f52c 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -27,7 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,) urlpatterns = patterns('', (r'^$', MockView.as_view()), - (r'^auth-token/', include('rest_framework.authtoken.urls')), + (r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'), ) From 8ebbae683bcdb1b70711df8e0cfaad760c4e8fd0 Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 17:03:10 -0800 Subject: [PATCH 13/79] Removed authtoken/urls.py, not really needed with Tom's simplification --- rest_framework/authtoken/urls.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 rest_framework/authtoken/urls.py diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py deleted file mode 100644 index a3419da60..000000000 --- a/rest_framework/authtoken/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Login view for token authentication. - -Add this to your root URLconf if you're using token authentication -your API requires authentication. - -You should make sure your authentication settings include -`TokenAuthentication`. - - urlpatterns = patterns('', - ... - url(r'^auth-token/', 'rest_framework.authtoken.obtain_auth_token') - ) -""" - -from django.conf.urls.defaults import patterns, url - -urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', 'rest_framework.authtoken.views.obtain_auth_token', name='token_login'), -) From 8b999c6bb500a045c6c32412009cbd3b1cd5a56b Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Wed, 14 Nov 2012 11:46:16 +0100 Subject: [PATCH 14/79] polishing code and adding myself to auhtors file --- docs/topics/credits.md | 2 ++ rest_framework/mixins.py | 4 ++-- rest_framework/tests/hyperlinkedserializers.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 22d08df7a..8e71c937a 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -59,6 +59,7 @@ The following people have helped make REST framework great. * Toni Michel - [tonimichel] * Ben Konrath - [benkonrath] * Marc Aymerich - [glic3rinu] +* Ludwig Kraatz - [ludwigkraatz] Many thanks to everyone who's contributed to the project. @@ -153,3 +154,4 @@ To contact the author directly: [tonimichel]: https://github.com/tonimichel [benkonrath]: https://github.com/benkonrath [glic3rinu]: https://github.com/glic3rinu +[ludwigkraatz]: https://github.com/ludwigkraatz diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f6119aa18..089425d55 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -24,8 +24,8 @@ class CreateModelMixin(object): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def get_success_headers(self,data): - if "url" in data: - return {'Location':data.get("url")} + if 'url' in data: + return {'Location': data.get('url')} else: return {} diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 5fc935ae4..d7effce70 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -196,7 +196,7 @@ class TestCreateWithForeignKeys(TestCase): request = factory.post('/comments/', data=data) response = self.create_view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response["Location"], 'http://testserver/comments/1/') + self.assertEqual(response['Location'], 'http://testserver/comments/1/') self.assertEqual(self.post.blogpostcomment_set.count(), 1) self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment') @@ -221,7 +221,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase): request = factory.post('/photos/', data=data) response = self.list_create_view(request).render() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertNotIn("Location", response, msg="Location should only be included if there is a 'url' field on the serializer") + self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer') self.assertEqual(self.post.photo_set.count(), 1) self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo') From d9c62c20a7a025d8e94fb881641731b340088f98 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz <ludi182@hotmail.com> Date: Wed, 14 Nov 2012 13:24:20 +0100 Subject: [PATCH 15/79] once more polished --- rest_framework/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 089425d55..cd104a7c9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -23,7 +23,7 @@ class CreateModelMixin(object): return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def get_success_headers(self,data): + def get_success_headers(self, data): if 'url' in data: return {'Location': data.get('url')} else: From 023b065ddc08735c487adff76cc62a864efe1697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 14 Nov 2012 16:02:50 +0100 Subject: [PATCH 16/79] added support for passing page_size per request --- rest_framework/mixins.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88e..f725fc9e9 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -55,6 +55,16 @@ class ListModelMixin(object): return Response(serializer.data) + def get_paginate_by(self, queryset): + page_size_param = self.request.QUERY_PARAMS.get('page_size') + if page_size_param: + try: + page_size = int(page_size_param) + return page_size + except ValueError: + pass + return super(ListModelMixin, self).get_paginate_by(queryset) + class RetrieveModelMixin(object): """ From 33fe0d2bea5ce391a69047236c121ec5b33d9b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 14 Nov 2012 16:08:14 +0100 Subject: [PATCH 17/79] added release note --- 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 35e8a8b35..40ce43521 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. +* Support for `page_size` GET parameter in view which inherit ListModelMixin ## 2.1.2 From 5967f15f7f5c87987ab60e4b7dc682b06f3ab511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 14 Nov 2012 16:11:35 +0100 Subject: [PATCH 18/79] updated docs --- docs/api-guide/generic-views.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 360ef1a2e..9e5119cbd 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -147,6 +147,10 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q Should be mixed in with [MultipleObjectAPIView]. +**Arguments**: + +* `page_size` - Hook to adjust page_size per request. + ## CreateModelMixin Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. From 1e83b60a43c26db921d6910092362feb3a76500d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 14 Nov 2012 18:00:59 +0100 Subject: [PATCH 19/79] added description how to use the auth token --- docs/api-guide/authentication.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 3137b9d4c..cb1e2645b 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -97,6 +97,21 @@ If successfully authenticated, `TokenAuthentication` provides the following cred **Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only. +If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. + + @receiver(post_save, sender=User) + def create_auth_token(sender, instance=None, created=False, **kwargs): + if created: + Token.objects.create(user=instance) + +If you've already created some User`'s, you can run a script like this. + + from django.contrib.auth.models import User + from rest_framework.authtoken.models import Token + + for user in User.objects.all(): + Token.objects.get_or_create(user=user) + ## OAuthAuthentication This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf. From 44ff2e0add240915d3217ba418178e58e2920419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 14 Nov 2012 19:36:29 +0100 Subject: [PATCH 20/79] fixed some typos --- rest_framework/compat.py | 2 +- rest_framework/decorators.py | 2 +- rest_framework/fields.py | 2 +- rest_framework/renderers.py | 2 +- rest_framework/response.py | 2 +- rest_framework/serializers.py | 10 +++++----- rest_framework/settings.py | 2 +- rest_framework/urlpatterns.py | 2 +- rest_framework/urls.py | 4 ++-- rest_framework/views.py | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5055bfd3e..e38e7c33a 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -1,6 +1,6 @@ """ The `compat` module provides support for backwards compatibility with older -versions of django/python, and compatbility wrappers around optional packages. +versions of django/python, and compatibility wrappers around optional packages. """ # flake8: noqa import django diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index a231f1913..1b710a03c 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -17,7 +17,7 @@ def api_view(http_method_names): ) # Note, the above allows us to set the docstring. - # It is the equivelent of: + # It is the equivalent of: # # class WrappedAPIView(APIView): # pass diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c2064261..6ef539754 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -317,7 +317,7 @@ class RelatedField(WritableField): choices = property(_get_choices, _set_choices) - ### Regular serializier stuff... + ### Regular serializer stuff... def field_to_native(self, obj, field_name): value = getattr(obj, self.source or field_name) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 22fd6e740..870464f0b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -4,7 +4,7 @@ Renderers are used to serialize a response into specific media types. They give us a generic way of being able to handle various media types on the response, such as JSON encoded data or HTML output. -REST framework also provides an HTML renderer the renders the browseable API. +REST framework also provides an HTML renderer the renders the browsable API. """ import copy import string diff --git a/rest_framework/response.py b/rest_framework/response.py index 0de01204d..0bd6c65d7 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -15,7 +15,7 @@ class Response(SimpleTemplateResponse): Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. - Setting 'renderer' and 'media_type' will typically be defered, + Setting 'renderer' and 'media_type' will typically be deferred, For example being set automatically by the `APIView`. """ super(Response, self).__init__(None, status=status) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46d4765e1..0f943ac15 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -89,7 +89,7 @@ class BaseSerializer(Field): pass _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatability with unsorted implementations. + _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations. def __init__(self, instance=None, data=None, context=None, **kwargs): super(BaseSerializer, self).__init__(**kwargs) @@ -163,7 +163,7 @@ class BaseSerializer(Field): self.opts.depth = parent.opts.depth - 1 ##### - # Methods to convert or revert from objects <--> primative representations. + # Methods to convert or revert from objects <--> primitive representations. def get_field_key(self, field_name): """ @@ -244,7 +244,7 @@ class BaseSerializer(Field): def to_native(self, obj): """ - Serialize objects -> primatives. + Serialize objects -> primitives. """ if hasattr(obj, '__iter__'): return [self.convert_object(item) for item in obj] @@ -252,7 +252,7 @@ class BaseSerializer(Field): def from_native(self, data): """ - Deserialize primatives -> objects. + Deserialize primitives -> objects. """ if hasattr(data, '__iter__') and not isinstance(data, dict): # TODO: error data when deserializing lists @@ -334,7 +334,7 @@ class ModelSerializer(Serializer): """ Return all the fields that should be serialized for the model. """ - # TODO: Modfiy this so that it's called on init, and drop + # TODO: Modify this so that it's called on init, and drop # serialize/obj/data arguments. # # We *could* provide a hook for dynamic fields, but diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 906a7cf6c..4f10481de 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -152,7 +152,7 @@ class APISettings(object): def validate_setting(self, attr, val): if attr == 'FILTER_BACKEND' and val is not None: - # Make sure we can initilize the class + # Make sure we can initialize the class val() api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 316ccd195..0ad926fa1 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -4,7 +4,7 @@ from rest_framework.settings import api_settings def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): """ - Supplement existing urlpatterns with corrosponding patterns that also + Supplement existing urlpatterns with corresponding patterns that also include a '.format' suffix. Retains urlpattern ordering. urlpatterns: diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 1a81101f2..bcdc23e74 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -1,7 +1,7 @@ """ -Login and logout views for the browseable API. +Login and logout views for the browsable API. -Add these to your root URLconf if you're using the browseable API and +Add these to your root URLconf if you're using the browsable API and your API requires authentication. The urls must be namespaced as 'rest_framework', and you should make sure diff --git a/rest_framework/views.py b/rest_framework/views.py index 1afbd6974..10bdd5a53 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -140,7 +140,7 @@ class APIView(View): def http_method_not_allowed(self, request, *args, **kwargs): """ - Called if `request.method` does not corrospond to a handler method. + Called if `request.method` does not correspond to a handler method. """ raise exceptions.MethodNotAllowed(request.method) From 647abcdb16105c51d3414d2da840aeb93b290ef9 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Wed, 14 Nov 2012 19:34:19 +0000 Subject: [PATCH 21/79] Bring keywrod args in line with Django's implementation --- rest_framework/generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ebd06e452..ddb604e0a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -92,11 +92,11 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): pk_url_kwarg = 'pk' # Not provided in Django 1.3 slug_url_kwarg = 'slug' # Not provided in Django 1.3 - def get_object(self): + def get_object(self, queryset=None): """ Override default to add support for object-level permissions. """ - obj = super(SingleObjectAPIView, self).get_object() + obj = super(SingleObjectAPIView, self).get_object(queryset) if not self.has_permission(self.request, obj): self.permission_denied(self.request) return obj From c35b9eb065b2a9cacaee1dc0849f01f3483e6130 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Wed, 14 Nov 2012 21:13:23 +0100 Subject: [PATCH 22/79] Processed review comments. No type checking in .restore_fields() Added missing BytesIO import. --- rest_framework/fields.py | 22 ++++++++++++++++------ rest_framework/serializers.py | 5 +---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 162d22714..dce31c5ab 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -3,6 +3,8 @@ import datetime import inspect import warnings +from io import BytesIO + from django.core import validators from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.urlresolvers import resolve, get_script_prefix @@ -31,6 +33,7 @@ class Field(object): creation_counter = 0 empty = '' type_name = None + _use_files = None def __init__(self, source=None): self.parent = None @@ -51,7 +54,7 @@ class Field(object): self.root = parent.root or parent self.context = self.root.context - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. @@ -166,7 +169,7 @@ class WritableField(Field): if errors: raise ValidationError(errors) - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): """ Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. @@ -175,7 +178,10 @@ class WritableField(Field): return try: - native = data[field_name] + if self._use_files: + native = files[field_name] + else: + native = data[field_name] except KeyError: if self.default is not None: native = self.default @@ -323,7 +329,7 @@ class RelatedField(WritableField): value = getattr(obj, self.source or field_name) return self.to_native(value) - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): if self.read_only: return @@ -341,7 +347,7 @@ class ManyRelatedMixin(object): value = getattr(obj, self.source or field_name) return [self.to_native(item) for item in value.all()] - def field_from_native(self, data, field_name, into): + def field_from_native(self, data, files, field_name, into): if self.read_only: return @@ -907,8 +913,10 @@ class FloatField(WritableField): class FileField(WritableField): + _use_files = True type_name = 'FileField' widget = widgets.FileInput + default_error_messages = { 'invalid': _("No file was submitted. Check the encoding type on the form."), 'missing': _("No file was submitted."), @@ -951,6 +959,8 @@ class FileField(WritableField): class ImageField(FileField): + _use_files = True + default_error_messages = { 'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."), } @@ -990,7 +1000,7 @@ class ImageField(FileField): # _imaging C module isn't available, so an ImportError will be # raised. Catch and re-raise. raise - except Exception: # Python Imaging Library doesn't recognize it as an image + except Exception: # Python Imaging Library doesn't recognize it as an image raise ValidationError(self.error_messages['invalid_image']) if hasattr(f, 'seek') and callable(f.seek): f.seek(0) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a46432a9c..4a13a0915 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -198,10 +198,7 @@ class BaseSerializer(Field): reverted_data = {} for field_name, field in fields.items(): try: - if isinstance(field, (FileField, ImageField)): - field.field_from_native(files, field_name, reverted_data) - else: - field.field_from_native(data, field_name, reverted_data) + field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: self._errors[field_name] = list(err.messages) From e112a806d863c0d6662dc3a65909ac191c02f03e Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Wed, 14 Nov 2012 21:40:52 +0100 Subject: [PATCH 23/79] .to_native() now returns the file-name. --- rest_framework/fields.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index dce31c5ab..fd57aa2c1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -952,10 +952,7 @@ class FileField(WritableField): return data def to_native(self, value): - """ - No need to return anything, the file can be accessed form its url. - """ - return + return value.name class ImageField(FileField): From 4a2526bd1e067104a1553a3e158016fe9ad285bb Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Sat, 10 Nov 2012 16:09:14 -0800 Subject: [PATCH 24/79] Added authtoken login/logout urlpatterns and views to support scripted logins and logouts using TokenAuthentication. Added unittests. --- rest_framework/authtoken/serializers.py | 37 +++++++++++++++++++++++++ rest_framework/authtoken/urls.py | 21 ++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 rest_framework/authtoken/serializers.py create mode 100644 rest_framework/authtoken/urls.py diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py new file mode 100644 index 000000000..8e0128c14 --- /dev/null +++ b/rest_framework/authtoken/serializers.py @@ -0,0 +1,37 @@ +from django.contrib.auth import authenticate + +from rest_framework import serializers +from rest_framework.authtoken.models import Token + + +class AuthTokenSerializer(serializers.Serializer): + token = serializers.Field(source="key") + username = serializers.CharField(max_length=30) + password = serializers.CharField() + + def validate(self, attrs): + username = attrs.get('username') + password = attrs.get('password') + + if username and password: + user = authenticate(username=username, password=password) + + if user: + if not user.is_active: + raise serializers.ValidationError('User account is disabled.') + attrs['user'] = user + return attrs + else: + raise serializers.ValidationError('Unable to login with provided credentials.') + else: + raise serializers.ValidationError('Must include "username" and "password"') + + def convert_object(self, obj): + ret = self._dict_class() + ret['token'] = obj.key + ret['user'] = obj.user.id + return ret + + def restore_object(self, attrs, instance=None): + token, created = Token.objects.get_or_create(user=attrs['user']) + return token diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py new file mode 100644 index 000000000..2a3e81150 --- /dev/null +++ b/rest_framework/authtoken/urls.py @@ -0,0 +1,21 @@ +""" +Login and logout views for token authentication. + +Add these to your root URLconf if you're using token authentication +your API requires authentication. + +The urls must be namespaced as 'rest_framework', and you should make sure +your authentication settings include `TokenAuthentication`. + + urlpatterns = patterns('', + ... + url(r'^auth-token', include('rest_framework.authtoken.urls', namespace='rest_framework')) + ) +""" +from django.conf.urls.defaults import patterns, url +from rest_framework.authtoken.views import AuthTokenView + +urlpatterns = patterns('rest_framework.authtoken.views', + url(r'^login/$', AuthTokenView.as_view(), name='token_login'), +# url(r'^logout/$', 'token_logout', name='token_logout'), +) From bd92db3c672137fa68185dbc0f453f7cea7caff3 Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Sat, 10 Nov 2012 16:17:50 -0800 Subject: [PATCH 25/79] Added authtoken login/logout urlpatterns and views --- rest_framework/authtoken/urls.py | 6 ++-- rest_framework/authtoken/views.py | 19 +++++++++++ rest_framework/tests/authentication.py | 46 +++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 2a3e81150..8bea46c00 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -13,9 +13,9 @@ your authentication settings include `TokenAuthentication`. ) """ from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenView +from rest_framework.authtoken.views import AuthTokenLoginView, AuthTokenLogoutView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenView.as_view(), name='token_login'), -# url(r'^logout/$', 'token_logout', name='token_logout'), + url(r'^login/$', AuthTokenLoginView.as_view(), name='token_login'), + url(r'^logout/$', AuthTokenLogoutView.as_view(), name='token_logout'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index e69de29bb..a52f0a77c 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -0,0 +1,19 @@ +from rest_framework.views import APIView +from rest_framework.generics import CreateAPIView +from rest_framework.authtoken.models import Token +from rest_framework.authtoken.serializers import AuthTokenSerializer +from django.http import HttpResponse + +class AuthTokenLoginView(CreateAPIView): + model = Token + serializer_class = AuthTokenSerializer + + +class AuthTokenLogoutView(APIView): + def post(self, request): + if request.user.is_authenticated() and request.auth: + request.auth.delete() + return HttpResponse("logged out") + else: + return HttpResponse("not logged in") + diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index 8ab4c4e40..d1bc23d9b 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns +from django.conf.urls.defaults import patterns, include from django.contrib.auth.models import User from django.test import Client, TestCase @@ -27,6 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,) urlpatterns = patterns('', (r'^$', MockView.as_view()), + (r'^auth-token/', include('rest_framework.authtoken.urls')), ) @@ -152,3 +153,46 @@ class TokenAuthTests(TestCase): self.token.delete() token = Token.objects.create(user=self.user) self.assertTrue(bool(token.key)) + + def test_token_login_json(self): + """Ensure token login view using JSON POST works.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username, 'password': self.password}), 'application/json') + self.assertEqual(response.status_code, 201) + self.assertEqual(json.loads(response.content)['token'], self.key) + + def test_token_login_json_bad_creds(self): + """Ensure token login view using JSON POST fails if bad credentials are used.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + self.assertEqual(response.status_code, 400) + + def test_token_login_json_missing_fields(self): + """Ensure token login view using JSON POST fails if missing fields.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + json.dumps({'username': self.username}), 'application/json') + self.assertEqual(response.status_code, 400) + + def test_token_login_form(self): + """Ensure token login view using form POST works.""" + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/login/', + {'username': self.username, 'password': self.password}) + self.assertEqual(response.status_code, 201) + self.assertEqual(json.loads(response.content)['token'], self.key) + + def test_token_logout(self): + """Ensure token logout view using JSON POST works.""" + # Use different User and Token as to isolate this test's effects on other unittests in class + username = "ringo" + user = User.objects.create_user(username, "starr@thebeatles.com", "pass") + token = Token.objects.create(user=user) + auth = "Token " + token.key + client = Client(enforce_csrf_checks=True) + response = client.post('/auth-token/logout/', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 200) + # Ensure token no longer exists + self.assertRaises(Token.DoesNotExist, lambda token: Token.objects.get(key=token.key), token) From ce3ccb91dc2a7aaf8ff41ac24045c558d641839e Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Mon, 12 Nov 2012 15:16:53 -0800 Subject: [PATCH 26/79] Updates to login view for TokenAuthentication from feedback from Tom --- rest_framework/authtoken/serializers.py | 15 +------------- rest_framework/authtoken/urls.py | 5 ++--- rest_framework/authtoken/views.py | 27 ++++++++++++++----------- rest_framework/tests/authentication.py | 25 ++++++----------------- 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 8e0128c14..a5ed6e6d7 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,12 +1,8 @@ from django.contrib.auth import authenticate - from rest_framework import serializers -from rest_framework.authtoken.models import Token - class AuthTokenSerializer(serializers.Serializer): - token = serializers.Field(source="key") - username = serializers.CharField(max_length=30) + username = serializers.CharField() password = serializers.CharField() def validate(self, attrs): @@ -26,12 +22,3 @@ class AuthTokenSerializer(serializers.Serializer): else: raise serializers.ValidationError('Must include "username" and "password"') - def convert_object(self, obj): - ret = self._dict_class() - ret['token'] = obj.key - ret['user'] = obj.user.id - return ret - - def restore_object(self, attrs, instance=None): - token, created = Token.objects.get_or_create(user=attrs['user']) - return token diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 8bea46c00..878721367 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -13,9 +13,8 @@ your authentication settings include `TokenAuthentication`. ) """ from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenLoginView, AuthTokenLogoutView +from rest_framework.authtoken.views import AuthTokenView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenLoginView.as_view(), name='token_login'), - url(r'^logout/$', AuthTokenLogoutView.as_view(), name='token_logout'), + url(r'^login/$', AuthTokenView.as_view(), name='token_login'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index a52f0a77c..e027dff1c 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -1,19 +1,22 @@ from rest_framework.views import APIView -from rest_framework.generics import CreateAPIView +from rest_framework import status +from rest_framework import parsers +from rest_framework import renderers +from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -from django.http import HttpResponse -class AuthTokenLoginView(CreateAPIView): +class AuthTokenView(APIView): + throttle_classes = () + permission_classes = () + parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) + renderer_classes = (renderers.JSONRenderer,) model = Token - serializer_class = AuthTokenSerializer - -class AuthTokenLogoutView(APIView): def post(self, request): - if request.user.is_authenticated() and request.auth: - request.auth.delete() - return HttpResponse("logged out") - else: - return HttpResponse("not logged in") - + serializer = AuthTokenSerializer(data=request.DATA) + if serializer.is_valid(): + token, created = Token.objects.get_or_create(user=serializer.object['user']) + return Response({'token': token.key}) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index d1bc23d9b..cb16ef1e8 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -158,41 +158,28 @@ class TokenAuthTests(TestCase): """Ensure token login view using JSON POST works.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username, 'password': self.password}), 'application/json') - self.assertEqual(response.status_code, 201) + json.dumps({'username': self.username, 'password': self.password}), 'application/json') + self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content)['token'], self.key) def test_token_login_json_bad_creds(self): """Ensure token login view using JSON POST fails if bad credentials are used.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_json_missing_fields(self): """Ensure token login view using JSON POST fails if missing fields.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - json.dumps({'username': self.username}), 'application/json') + json.dumps({'username': self.username}), 'application/json') self.assertEqual(response.status_code, 400) def test_token_login_form(self): """Ensure token login view using form POST works.""" client = Client(enforce_csrf_checks=True) response = client.post('/auth-token/login/', - {'username': self.username, 'password': self.password}) - self.assertEqual(response.status_code, 201) - self.assertEqual(json.loads(response.content)['token'], self.key) - - def test_token_logout(self): - """Ensure token logout view using JSON POST works.""" - # Use different User and Token as to isolate this test's effects on other unittests in class - username = "ringo" - user = User.objects.create_user(username, "starr@thebeatles.com", "pass") - token = Token.objects.create(user=user) - auth = "Token " + token.key - client = Client(enforce_csrf_checks=True) - response = client.post('/auth-token/logout/', HTTP_AUTHORIZATION=auth) + {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, 200) - # Ensure token no longer exists - self.assertRaises(Token.DoesNotExist, lambda token: Token.objects.get(key=token.key), token) + self.assertEqual(json.loads(response.content)['token'], self.key) From eb20b5663e68ce01453eeb855922874001f42d0f Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 15:03:42 -0800 Subject: [PATCH 27/79] Added documentation on how to use the token authentication login view. --- docs/api-guide/authentication.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index cb1e2645b..a55059a85 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -112,6 +112,21 @@ If you've already created some User`'s, you can run a script like this. for user in User.objects.all(): Token.objects.get_or_create(user=user) +When using TokenAuthentication, it may be useful to add a login view for clients to retrieve the token. + +REST framework provides a built-in login view. To use it, add a pattern to include the token login view for clients as follows: + + urlpatterns += patterns('', + url(r'^api-token-auth/', include('rest_framework.authtoken.urls', + namespace='rest_framework')) + ) + +The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. + +The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: + + { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } + ## OAuthAuthentication This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf. From 321ba156ca45da8a4b3328c4aec6a9235f32e5f8 Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 16:49:13 -0800 Subject: [PATCH 28/79] Renamed AuthTokenView to ObtainAuthToken, added obtain_auth_token var, updated tests & docs. Left authtoken.urls in place as example. --- docs/api-guide/authentication.md | 11 ++++------- docs/topics/credits.md | 3 +++ docs/topics/release-notes.md | 1 + rest_framework/authtoken/urls.py | 14 +++++++------- rest_framework/authtoken/views.py | 4 +++- rest_framework/tests/authentication.py | 2 +- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index a55059a85..a30bd22c1 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -114,18 +114,15 @@ If you've already created some User`'s, you can run a script like this. When using TokenAuthentication, it may be useful to add a login view for clients to retrieve the token. -REST framework provides a built-in login view. To use it, add a pattern to include the token login view for clients as follows: +REST framework provides a built-in login view for clients to retrieve the token called `rest_framework.authtoken.obtain_auth_token`. To use it, add a pattern to include the token login view for clients as follows: urlpatterns += patterns('', - url(r'^api-token-auth/', include('rest_framework.authtoken.urls', - namespace='rest_framework')) + url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token') ) -The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace. +The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: -The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: - - { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } + { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } ## OAuthAuthentication diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 8e71c937a..939dfc571 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -60,6 +60,7 @@ The following people have helped make REST framework great. * Ben Konrath - [benkonrath] * Marc Aymerich - [glic3rinu] * Ludwig Kraatz - [ludwigkraatz] +* Rob Romano - [robromano] Many thanks to everyone who's contributed to the project. @@ -155,3 +156,5 @@ To contact the author directly: [benkonrath]: https://github.com/benkonrath [glic3rinu]: https://github.com/glic3rinu [ludwigkraatz]: https://github.com/ludwigkraatz +[robromano]: https://github.com/robromano + diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 35e8a8b35..daacc76f5 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. +* Add convenience login view to get tokens when using `TokenAuthentication` ## 2.1.2 diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 878721367..a3419da60 100644 --- a/rest_framework/authtoken/urls.py +++ b/rest_framework/authtoken/urls.py @@ -1,20 +1,20 @@ """ -Login and logout views for token authentication. +Login view for token authentication. -Add these to your root URLconf if you're using token authentication +Add this to your root URLconf if you're using token authentication your API requires authentication. -The urls must be namespaced as 'rest_framework', and you should make sure -your authentication settings include `TokenAuthentication`. +You should make sure your authentication settings include +`TokenAuthentication`. urlpatterns = patterns('', ... - url(r'^auth-token', include('rest_framework.authtoken.urls', namespace='rest_framework')) + url(r'^auth-token/', 'rest_framework.authtoken.obtain_auth_token') ) """ + from django.conf.urls.defaults import patterns, url -from rest_framework.authtoken.views import AuthTokenView urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', AuthTokenView.as_view(), name='token_login'), + url(r'^login/$', 'rest_framework.authtoken.views.obtain_auth_token', name='token_login'), ) diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index e027dff1c..3ac674e28 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer -class AuthTokenView(APIView): +class ObtainAuthToken(APIView): throttle_classes = () permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) @@ -20,3 +20,5 @@ class AuthTokenView(APIView): return Response({'token': token.key}) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +obtain_auth_token = ObtainAuthToken.as_view() diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index cb16ef1e8..96ca9f52c 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -27,7 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,) urlpatterns = patterns('', (r'^$', MockView.as_view()), - (r'^auth-token/', include('rest_framework.authtoken.urls')), + (r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'), ) From 535b65a34862e1dcf6370046a37e9cd0e980f491 Mon Sep 17 00:00:00 2001 From: Rob Romano <rromano@gmail.com> Date: Tue, 13 Nov 2012 17:03:10 -0800 Subject: [PATCH 29/79] Removed authtoken/urls.py, not really needed with Tom's simplification --- rest_framework/authtoken/urls.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 rest_framework/authtoken/urls.py diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py deleted file mode 100644 index a3419da60..000000000 --- a/rest_framework/authtoken/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -Login view for token authentication. - -Add this to your root URLconf if you're using token authentication -your API requires authentication. - -You should make sure your authentication settings include -`TokenAuthentication`. - - urlpatterns = patterns('', - ... - url(r'^auth-token/', 'rest_framework.authtoken.obtain_auth_token') - ) -""" - -from django.conf.urls.defaults import patterns, url - -urlpatterns = patterns('rest_framework.authtoken.views', - url(r'^login/$', 'rest_framework.authtoken.views.obtain_auth_token', name='token_login'), -) From 69a01d71256b9923aac1b8d1b91063068ecfebf7 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Wed, 14 Nov 2012 23:04:46 +0100 Subject: [PATCH 30/79] Added a test for the FileField. --- rest_framework/tests/files.py | 55 +++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 61d7f7b16..5dd57b7c6 100644 --- a/rest_framework/tests/files.py +++ b/rest_framework/tests/files.py @@ -1,34 +1,39 @@ -# from django.test import TestCase -# from django import forms +import StringIO +import datetime -# from django.test.client import RequestFactory -# from rest_framework.views import View -# from rest_framework.response import Response +from django.test import TestCase -# import StringIO +from rest_framework import serializers -# class UploadFilesTests(TestCase): -# """Check uploading of files""" -# def setUp(self): -# self.factory = RequestFactory() +class UploadedFile(object): + def __init__(self, file, created=None): + self.file = file + self.created = created or datetime.datetime.now() -# def test_upload_file(self): -# class FileForm(forms.Form): -# file = forms.FileField() +class UploadedFileSerializer(serializers.Serializer): + file = serializers.FileField() + created = serializers.DateTimeField() -# class MockView(View): -# permissions = () -# form = FileForm + def restore_object(self, attrs, instance=None): + if instance: + instance.file = attrs['file'] + instance.created = attrs['created'] + return instance + return UploadedFile(**attrs) -# def post(self, request, *args, **kwargs): -# return Response({'FILE_NAME': self.CONTENT['file'].name, -# 'FILE_CONTENT': self.CONTENT['file'].read()}) -# file = StringIO.StringIO('stuff') -# file.name = 'stuff.txt' -# request = self.factory.post('/', {'file': file}) -# view = MockView.as_view() -# response = view(request) -# self.assertEquals(response.raw_content, {"FILE_CONTENT": "stuff", "FILE_NAME": "stuff.txt"}) +class FileSerializerTests(TestCase): + + def test_create(self): + now = datetime.datetime.now() + file = StringIO.StringIO('stuff') + file.name = 'stuff.txt' + file.size = file.len + serializer = UploadedFileSerializer(data={'created': now}, files={'file': file}) + uploaded_file = UploadedFile(file=file, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.object.created, uploaded_file.created) + self.assertEquals(serializer.object.file, uploaded_file.file) + self.assertFalse(serializer.object is uploaded_file) From 3b258d69c92e9d9293f7c5d1690f0ca434e677e3 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Wed, 14 Nov 2012 23:24:18 +0100 Subject: [PATCH 31/79] Fix 404 Fixes #417 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 644df873d..c3ffc9a7d 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [twitter]: https://twitter.com/_tomchristie [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [sandbox]: http://restframework.herokuapp.com/ -[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md +[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [docs]: http://django-rest-framework.org/ From 38e94bb8b4e04249b18b9b57ef2ddcb7cfc4efa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Thu, 15 Nov 2012 11:15:05 +0100 Subject: [PATCH 32/79] added global and per resource on/off switch + updated docs --- docs/api-guide/generic-views.md | 3 ++- docs/api-guide/settings.md | 6 ++++++ rest_framework/mixins.py | 18 +++++++++++------- rest_framework/settings.py | 4 +++- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 9e5119cbd..734a91e92 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -149,7 +149,8 @@ Should be mixed in with [MultipleObjectAPIView]. **Arguments**: -* `page_size` - Hook to adjust page_size per request. +* `allow_page_size_param` - Allows you to overwrite the global settings `ALLOW_PAGE_SIZE_PARAM` for a specific view. +* `page_size_param` - Allows you to customize the page_size parameter. Default is `page_size`. ## CreateModelMixin diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 4f87b30da..2f90369b4 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -150,4 +150,10 @@ Default: `'accept'` Default: `'format'` +## ALLOW_PAGE_SIZE_PARAM + +Allows you to globally pass a page size parameter for an individual request. + +Default: `'True'` + [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f725fc9e9..d64e7e56a 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,6 +7,7 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response +from rest_framework.settings import api_settings class CreateModelMixin(object): @@ -32,6 +33,8 @@ class ListModelMixin(object): Should be mixed in with `MultipleObjectAPIView`. """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." + allow_page_size_param = api_settings.ALLOW_PAGE_SIZE_PARAM + page_size_param = 'page_size' def list(self, request, *args, **kwargs): self.object_list = self.get_filtered_queryset() @@ -56,13 +59,14 @@ class ListModelMixin(object): return Response(serializer.data) def get_paginate_by(self, queryset): - page_size_param = self.request.QUERY_PARAMS.get('page_size') - if page_size_param: - try: - page_size = int(page_size_param) - return page_size - except ValueError: - pass + if self.allow_page_size_param: + page_size_param = self.request.QUERY_PARAMS.get(self.page_size_param) + if page_size_param: + try: + page_size = int(page_size_param) + return page_size + except ValueError: + pass return super(ListModelMixin, self).get_paginate_by(queryset) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 906a7cf6c..1daa9dfdb 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -66,7 +66,9 @@ DEFAULTS = { 'URL_ACCEPT_OVERRIDE': 'accept', 'URL_FORMAT_OVERRIDE': 'format', - 'FORMAT_SUFFIX_KWARG': 'format' + 'FORMAT_SUFFIX_KWARG': 'format', + + 'ALLOW_PAGE_SIZE_PARAM': True } From b17a9818008cf3828adb896ae9be134fb63c5693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Thu, 15 Nov 2012 11:24:17 +0100 Subject: [PATCH 33/79] updated release noted for page_size stuff --- docs/topics/release-notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 40ce43521..f4a76c899 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,7 +7,9 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. -* Support for `page_size` GET parameter in view which inherit ListModelMixin +* Support for `page_size` GET parameter in views which inherit ListModelMixin. +* Support for customizing `page_size` param via `page_size_param` attribute. +* Support for allowing `page_size` param globally (via `ALLOW_PAGE_SIZE_PARAM`) and for individual views (via `allow_page_size_param`) ## 2.1.2 From 3ae203a0184d27318a8a828ce322b151ade0340f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Thu, 15 Nov 2012 12:06:43 +0100 Subject: [PATCH 34/79] updated script to just use page_size_kwarg --- docs/api-guide/generic-views.md | 3 +-- docs/api-guide/settings.md | 8 ++++++-- docs/topics/release-notes.md | 4 +--- rest_framework/mixins.py | 11 +++++------ rest_framework/settings.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 734a91e92..3346c70a3 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -149,8 +149,7 @@ Should be mixed in with [MultipleObjectAPIView]. **Arguments**: -* `allow_page_size_param` - Allows you to overwrite the global settings `ALLOW_PAGE_SIZE_PARAM` for a specific view. -* `page_size_param` - Allows you to customize the page_size parameter. Default is `page_size`. +* `page_size_kwarg` - Allows you to overwrite the global settings `PAGE_SIZE_KWARG` for a specific view. You can also turn it off for a specific view by setting it to `None`. Default is `page_size`. ## CreateModelMixin diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 2f90369b4..8fce9e4e7 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -150,10 +150,14 @@ Default: `'accept'` Default: `'format'` -## ALLOW_PAGE_SIZE_PARAM +## PAGE_SIZE_KWARG Allows you to globally pass a page size parameter for an individual request. -Default: `'True'` +The name of the GET parameter of views which inherit ListModelMixin for requesting data with an individual page size. + +If the value if this setting is `None` the passing a page size is turned off by default. + +Default: `'page_size'` [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f4a76c899..85c19f5bc 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,9 +7,7 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. -* Support for `page_size` GET parameter in views which inherit ListModelMixin. -* Support for customizing `page_size` param via `page_size_param` attribute. -* Support for allowing `page_size` param globally (via `ALLOW_PAGE_SIZE_PARAM`) and for individual views (via `allow_page_size_param`) +* Support for individual page sizes per request via `page_size` GET parameter in views which inherit ListModelMixin. ## 2.1.2 diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index d64e7e56a..d85e0bfbb 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -33,8 +33,7 @@ class ListModelMixin(object): Should be mixed in with `MultipleObjectAPIView`. """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." - allow_page_size_param = api_settings.ALLOW_PAGE_SIZE_PARAM - page_size_param = 'page_size' + page_size_kwarg = api_settings.PAGE_SIZE_KWARG def list(self, request, *args, **kwargs): self.object_list = self.get_filtered_queryset() @@ -59,11 +58,11 @@ class ListModelMixin(object): return Response(serializer.data) def get_paginate_by(self, queryset): - if self.allow_page_size_param: - page_size_param = self.request.QUERY_PARAMS.get(self.page_size_param) - if page_size_param: + if self.page_size_kwarg is not None: + page_size_kwarg = self.request.QUERY_PARAMS.get(self.page_size_kwarg) + if page_size_kwarg: try: - page_size = int(page_size_param) + page_size = int(page_size_kwarg) return page_size except ValueError: pass diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 1daa9dfdb..c7b0643fb 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -68,7 +68,7 @@ DEFAULTS = { 'FORMAT_SUFFIX_KWARG': 'format', - 'ALLOW_PAGE_SIZE_PARAM': True + 'PAGE_SIZE_KWARG': 'page_size' } From a701a21587a69ed959533cbcfdaa9c63337c3ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Thu, 15 Nov 2012 14:35:34 +0100 Subject: [PATCH 35/79] added page_size_kwarg tests --- rest_framework/tests/pagination.py | 143 ++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 713a7255b..8aae21471 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -34,6 +34,29 @@ if django_filters: filter_backend = filters.DjangoFilterBackend +class DefaultPageSizeKwargView(generics.ListAPIView): + """ + View for testing default page_size usage + """ + model = BasicModel + + +class CustomPageSizeKwargView(generics.ListAPIView): + """ + View for testing custom page_size usage + """ + model = BasicModel + page_size_kwarg = 'ps' + + +class NonePageSizeKwargView(generics.ListAPIView): + """ + View for testing None page_size usage + """ + model = BasicModel + page_size_kwarg = None + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -135,7 +158,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): class UnitTestPagination(TestCase): """ - Unit tests for pagination of primative objects. + Unit tests for pagination of primitive objects. """ def setUp(self): @@ -156,3 +179,121 @@ class UnitTestPagination(TestCase): self.assertEquals(serializer.data['next'], None) self.assertEquals(serializer.data['previous'], '?page=2') self.assertEquals(serializer.data['results'], self.objects[20:]) + + +class TestDefaultPageSizeKwarg(TestCase): + """ + Tests for list views with default page size kwarg + """ + + def setUp(self): + """ + Create 13 BasicModel instances. + """ + for i in range(13): + BasicModel(text=i).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = DefaultPageSizeKwargView.as_view() + + def test_default_page_size(self): + """ + Tests the default page size for this view. + no page size --> no limit --> no meta data + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEquals(response.data, self.data) + + def test_default_page_size_kwarg(self): + """ + If page_size_kwarg is set not set, the default page_size kwarg should limit per view requests. + """ + request = factory.get('/?page_size=5') + response = self.view(request).render() + self.assertEquals(response.data['count'], 13) + self.assertEquals(response.data['results'], self.data[:5]) + + +class TestCustomPageSizeKwarg(TestCase): + """ + Tests for list views with default page size kwarg + """ + + def setUp(self): + """ + Create 13 BasicModel instances. + """ + for i in range(13): + BasicModel(text=i).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = CustomPageSizeKwargView.as_view() + + def test_default_page_size(self): + """ + Tests the default page size for this view. + no page size --> no limit --> no meta data + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEquals(response.data, self.data) + + def test_disabled_default_page_size_kwarg(self): + """ + If page_size_kwarg is set set, the default page_size kwarg should not work. + """ + request = factory.get('/?page_size=5') + response = self.view(request).render() + self.assertEquals(response.data, self.data) + + def test_custom_page_size_kwarg(self): + """ + If page_size_kwarg is set set, the new kwarg should limit per view requests. + """ + request = factory.get('/?ps=5') + response = self.view(request).render() + self.assertEquals(response.data['count'], 13) + self.assertEquals(response.data['results'], self.data[:5]) + + +class TestNonePageSizeKwarg(TestCase): + """ + Tests for list views with default page size kwarg + """ + + def setUp(self): + """ + Create 13 BasicModel instances. + """ + for i in range(13): + BasicModel(text=i).save() + self.objects = BasicModel.objects + self.data = [ + {'id': obj.id, 'text': obj.text} + for obj in self.objects.all() + ] + self.view = NonePageSizeKwargView.as_view() + + def test_default_page_size(self): + """ + Tests the default page size for this view. + no page size --> no limit --> no meta data + """ + request = factory.get('/') + response = self.view(request).render() + self.assertEquals(response.data, self.data) + + def test_none_page_size_kwarg(self): + """ + If page_size_kwarg is set to None, custom page_size per request should be disabled. + """ + request = factory.get('/?page_size=5') + response = self.view(request).render() + self.assertEquals(response.data, self.data) From cba181f4bce6684a45aa869cae0b2cca0c35eee0 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Thu, 15 Nov 2012 23:31:53 +0100 Subject: [PATCH 36/79] ./mkdocs.py -p opens a preview in your default browser. Tested on Mac, but should work on windows and Linux as well. --- mkdocs.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mkdocs.py b/mkdocs.py index 8106e8e22..2918f7d3b 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -11,6 +11,7 @@ docs_dir = os.path.join(root_dir, 'docs') html_dir = os.path.join(root_dir, 'html') local = not '--deploy' in sys.argv +preview = '-p' in sys.argv if local: base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir)) @@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output) output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output) open(output_path, 'w').write(output.encode('utf-8')) + +if preview: + import subprocess + + url = 'html/index.html' + + try: + subprocess.Popen(["open", url]) # Mac + except OSError: + subprocess.Popen(["xdg-open", url]) # Linux + except: + os.startfile(url) # Windows From b4cfb46a56c8f7d9bc4340d5443f3a2d57ba9b58 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 00:22:08 +0100 Subject: [PATCH 37/79] WIP on docs for File- and ImageFileds. --- docs/api-guide/fields.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0485b158f..0b25a6efd 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -165,6 +165,38 @@ A floating point representation. Corresponds to `django.db.models.fields.FloatField`. +## FileField + +A file representation. Performs Django's standard FileField validation. + +Corresponds to `django.forms.fields.FileField`. + +### Optional arguments + +#### `max_length` + +Maximum length for the file name. This value is obtained from the model when used with a ModelSerializer. + +Defaults to `None`, meaning validation is skipped. + +#### `allow_empty_file` + +Determines if empty file uploads are allowed. + +Defaults to `False` + +## ImageField + +An image representation. + +Corresponds to `django.forms.fields.ImageField`. + +### Optional arguments + +Same as FileField. + +Requires the `PIL` package. + --- # Relational Fields From c5765641a44ad2fb3b80f63f9a47e0dd7f432c94 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 17:28:08 +0000 Subject: [PATCH 38/79] Fix typo --- docs/api-guide/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 14ab9a26e..95d9d5260 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -71,7 +71,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ by filtering against a `username` query parameter in the URL. """ queryset = Purchase.objects.all() - username = self.request.QUERY_PARAMS.get('username', None): + username = self.request.QUERY_PARAMS.get('username', None) if username is not None: queryset = queryset.filter(purchaser__username=username) return queryset From 4edc801d5912b2c31855647b432e461e35322511 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 21:42:04 +0100 Subject: [PATCH 39/79] Reproduces #421 --- rest_framework/tests/serializer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 059593a90..a51df1465 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -239,6 +239,14 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), True) self.assertEquals(serializer.errors, {}) + def test_modelserializer_max_length_exceeded(self): + data = { + 'title': 'x' * 201, + } + serializer = ActionItemSerializer(data=data) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 200 characters (it has 201).']}) + class MetadataTests(TestCase): def test_empty(self): From 2f2bde69e42825ad55318e5a5745ee9655b3f81b Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 20:58:49 +0000 Subject: [PATCH 40/79] Docs, tox and travis use django-filter 0.5.4 --- .travis.yml | 3 +-- docs/api-guide/filtering.md | 5 ++--- docs/index.md | 4 ++-- tox.ini | 12 ++++++------ 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 800ba2413..ccfdeacbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,8 +11,7 @@ env: install: - pip install $DJANGO - - pip install -r requirements.txt --use-mirrors - - pip install -e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + - pip install django-filter==0.5.4 --use-mirrors - export PYTHONPATH=. script: diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 95d9d5260..53ea7cbcc 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -84,9 +84,9 @@ As well as being able to override the default queryset, REST framework also incl REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package. -To use REST framework's default filtering backend, first install `django-filter`. +To use REST framework's filtering backend, first install `django-filter`. - pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter + pip install django-filter You must also set the filter backend to `DjangoFilterBackend` in your settings: @@ -94,7 +94,6 @@ You must also set the filter backend to `DjangoFilterBackend` in your settings: 'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend' } -**Note**: The currently supported version of `django-filter` is the `master` branch. A PyPI release is expected to be coming soon. ## Specifying filter fields diff --git a/docs/index.md b/docs/index.md index fd8345402..cc0f2a139 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,7 +34,7 @@ The following packages are optional: * [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API. * [PyYAML][yaml] (3.10+) - YAML content-type support. -* [django-filter][django-filter] (master) - Filtering support. +* [django-filter][django-filter] (0.5.4+) - Filtering support. ## Installation @@ -43,7 +43,7 @@ Install using `pip`, including any optional packages you want... pip install djangorestframework pip install markdown # Markdown support for the browseable API. pip install pyyaml # YAML content-type support. - pip install -e git+https://github.com/alex/django-filter.git#egg=django-filter # Filtering support + pip install django-filter # Filtering support ...or clone the project from github. diff --git a/tox.ini b/tox.ini index 3596bbdc3..69eb38237 100644 --- a/tox.ini +++ b/tox.ini @@ -8,29 +8,29 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py2.7-django1.5] basepython = python2.7 deps = https://github.com/django/django/zipball/master - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 [testenv:py2.7-django1.4] basepython = python2.7 deps = django==1.4.1 - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 [testenv:py2.7-django1.3] basepython = python2.7 deps = django==1.3.3 - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 [testenv:py2.6-django1.5] basepython = python2.6 deps = https://github.com/django/django/zipball/master - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.1 - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 [testenv:py2.6-django1.3] basepython = python2.6 deps = django==1.3.3 - git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter + django-filter==0.5.4 From aa013a428948802dff9c8ca00df3b7af6faf139b Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 22:18:57 +0100 Subject: [PATCH 41/79] Fixes #421 --- rest_framework/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f943ac15..e5c057fbe 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -427,6 +427,12 @@ class ModelSerializer(Serializer): kwargs['choices'] = model_field.flatchoices return ChoiceField(**kwargs) + max_length = getattr(model_field, 'max_length', None) + if max_length: + if not isinstance(model_field, models.CharField): + import pdb; pdb.set_trace() + kwargs['max_length'] = max_length + field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, From f385b72d80b7e9767a6f345496fd108ccc66a4bc Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 22:20:26 +0100 Subject: [PATCH 42/79] =?UTF-8?q?Whoops=20=E2=80=A6=20Drop=20pdb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e5c057fbe..8f4b7ae20 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -429,8 +429,6 @@ class ModelSerializer(Serializer): max_length = getattr(model_field, 'max_length', None) if max_length: - if not isinstance(model_field, models.CharField): - import pdb; pdb.set_trace() kwargs['max_length'] = max_length field_mapping = { From 8d3581f4bd9b0abbf88a7713a1cb8b67f820602a Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 21:27:34 +0000 Subject: [PATCH 43/79] Minor tweaks to internals of generics and mixins --- rest_framework/generics.py | 3 --- rest_framework/mixins.py | 27 +++++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ddb604e0a..9ad03f71c 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -66,9 +66,6 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): backend = self.filter_backend() return backend.filter_queryset(self.request, queryset, self) - def get_filtered_queryset(self): - return self.filter_queryset(self.get_queryset()) - def get_pagination_serializer_class(self): """ Return the class to use for the pagination serializer. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index cd104a7c9..53c4d9842 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -22,13 +22,13 @@ class CreateModelMixin(object): headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - + def get_success_headers(self, data): - if 'url' in data: - return {'Location': data.get('url')} - else: + try: + return {'Location': data['url']} + except (TypeError, KeyError): return {} - + def pre_save(self, obj): pass @@ -41,14 +41,16 @@ class ListModelMixin(object): empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): - self.object_list = self.get_filtered_queryset() + queryset = self.get_queryset() + self.object_list = self.filter_queryset(queryset) # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. allow_empty = self.get_allow_empty() - if not allow_empty and len(self.object_list) == 0: - error_args = {'class_name': self.__class__.__name__} - raise Http404(self.empty_error % error_args) + if not allow_empty and not self.object_list: + class_name = self.__class__.__name__ + error_msg = self.empty_error % {'class_name': class_name} + raise Http404(error_msg) # Pagination size is set by the `.paginate_by` attribute, # which may be `None` to disable pagination. @@ -82,17 +84,18 @@ class UpdateModelMixin(object): def update(self, request, *args, **kwargs): try: self.object = self.get_object() - success_status = status.HTTP_200_OK + created = False except Http404: self.object = None - success_status = status.HTTP_201_CREATED + created = True serializer = self.get_serializer(self.object, data=request.DATA) if serializer.is_valid(): self.pre_save(serializer.object) self.object = serializer.save() - return Response(serializer.data, status=success_status) + status_code = created and status.HTTP_201_CREATED or status.HTTP_200_OK + return Response(serializer.data, status=status_code) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) From 1a436dd6d9f56b62de61c55c89084d60c09966ba Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 22:43:16 +0100 Subject: [PATCH 44/79] Added URLField and SlugField. Fixed test_modelserializer_max_length_exceeded --- rest_framework/fields.py | 17 +++++++++++++++++ rest_framework/serializers.py | 2 ++ rest_framework/tests/serializer.py | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6ef539754..641a1417e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -700,6 +700,23 @@ class CharField(WritableField): return smart_unicode(value) +class URLField(CharField): + type_name = 'URLField' + + def __init__(self, **kwargs): + kwargs['max_length'] = kwargs.get('max_length', 200) + kwargs['validators'] = [validators.URLValidator()] + super(URLField, self).__init__(**kwargs) + + +class SlugField(CharField): + type_name = 'SlugField' + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = kwargs.get('max_length', 50) + super(SlugField, self).__init__(*args, **kwargs) + + class ChoiceField(WritableField): type_name = 'ChoiceField' widget = widgets.Select diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8f4b7ae20..dbd9fe271 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -441,6 +441,8 @@ class ModelSerializer(Serializer): models.DateField: DateField, models.EmailField: EmailField, models.CharField: CharField, + models.URLField: URLField, + models.SlugField: SlugField, models.TextField: CharField, models.CommaSeparatedIntegerField: CharField, models.BooleanField: BooleanField, diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a51df1465..fb1be7eb0 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -245,7 +245,7 @@ class ValidationTests(TestCase): } serializer = ActionItemSerializer(data=data) self.assertEquals(serializer.is_valid(), False) - self.assertEquals(serializer.errors, {'content': [u'Ensure this value has at most 200 characters (it has 201).']}) + self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']}) class MetadataTests(TestCase): From 0076e2f462402dbb7bd7b3a446d2c397e6bf8d81 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 23:23:34 +0100 Subject: [PATCH 45/79] Added brief docs for URLField and SlugField. --- docs/api-guide/fields.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0485b158f..5977cae2e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -131,6 +131,18 @@ or `django.db.models.fields.TextField`. **Signature:** `CharField(max_length=None, min_length=None)` +## URLField + +Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation. + +**Signature:** `CharField(max_length=200, min_length=None)` + +## SlugField + +Corresponds to `django.db.models.fields.SlugField`. + +**Signature:** `CharField(max_length=50, min_length=None)` + ## ChoiceField A field that can accept a value out of a limited set of choices. From f801e5d3050591403de04fde7d18522fabc8fe49 Mon Sep 17 00:00:00 2001 From: Marko Tibold <marko@tibold.nl> Date: Fri, 16 Nov 2012 23:44:55 +0100 Subject: [PATCH 46/79] Simplified docs a bit for FileField and ImageField. Added note about MultipartParser only supporting file uploads and Django's default upload handlers. --- docs/api-guide/fields.md | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0b25a6efd..7f42dc5e1 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -171,19 +171,11 @@ A file representation. Performs Django's standard FileField validation. Corresponds to `django.forms.fields.FileField`. -### Optional arguments +**Signature:** `FileField(max_length=None, allow_empty_file=False)` -#### `max_length` - -Maximum length for the file name. This value is obtained from the model when used with a ModelSerializer. - -Defaults to `None`, meaning validation is skipped. - -#### `allow_empty_file` - -Determines if empty file uploads are allowed. - -Defaults to `False` + - `max_length` designates the maximum length for the file name. + + - `allow_empty_file` designates if empty files are allowed. ## ImageField @@ -191,12 +183,15 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -### Optional arguments - -Same as FileField. - Requires the `PIL` package. +Signature and validation is the same as with `FileField`. + +--- + +**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads. +Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. + --- # Relational Fields @@ -318,3 +313,4 @@ 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`. [cite]: http://www.python.org/dev/peps/pep-0020/ +[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS From 31f01bd6315f46bf28bb4c9c25a5298785fc4fc6 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 22:45:57 +0000 Subject: [PATCH 47/79] Polishing to page size query parameters & more docs --- docs/api-guide/generic-views.md | 22 ++++++-- docs/api-guide/settings.md | 26 +++++---- docs/topics/release-notes.md | 3 +- rest_framework/generics.py | 32 ++++++++--- rest_framework/mixins.py | 13 ----- rest_framework/settings.py | 9 +++- rest_framework/tests/pagination.py | 85 +++++------------------------- 7 files changed, 79 insertions(+), 111 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 3346c70a3..33ec89d28 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -123,18 +123,36 @@ Each of the generic views provided is built by combining one of the base views b Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets. +**Attributes**: + +* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required. +* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class. + ## MultipleObjectAPIView Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin]. **See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy]. +**Attributes**: + +* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`. +* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`. +* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`. + ## SingleObjectAPIView Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin]. **See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy]. +**Attributes**: + +* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`. +* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+] +* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+] +* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`. + --- # Mixins @@ -147,10 +165,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q Should be mixed in with [MultipleObjectAPIView]. -**Arguments**: - -* `page_size_kwarg` - Allows you to overwrite the global settings `PAGE_SIZE_KWARG` for a specific view. You can also turn it off for a specific view by setting it to `None`. Default is `page_size`. - ## CreateModelMixin Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 8fce9e4e7..7884d096b 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -96,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer` Default: `rest_framework.pagination.PaginationSerializer` -## FORMAT_SUFFIX_KWARG +## FILTER_BACKEND -**TODO** +The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled. -Default: `'format'` +## PAGINATE_BY + +The default page size to use for pagination. If set to `None`, pagination is disabled by default. + +Default: `None` + +## PAGINATE_BY_KWARG + +The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size. + +Default: `None` ## UNAUTHENTICATED_USER @@ -150,14 +160,10 @@ Default: `'accept'` Default: `'format'` -## PAGE_SIZE_KWARG +## FORMAT_SUFFIX_KWARG -Allows you to globally pass a page size parameter for an individual request. +**TODO** -The name of the GET parameter of views which inherit ListModelMixin for requesting data with an individual page size. - -If the value if this setting is `None` the passing a page size is turned off by default. - -Default: `'page_size'` +Default: `'format'` [cite]: http://www.python.org/dev/peps/pep-0020/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 85c19f5bc..869cabc89 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,7 +7,8 @@ ## Master * Support for `read_only_fields` on `ModelSerializer` classes. -* Support for individual page sizes per request via `page_size` GET parameter in views which inherit ListModelMixin. +* 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. ## 2.1.2 diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 9ad03f71c..dcf4dfd9b 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,6 +14,7 @@ class GenericAPIView(views.APIView): """ Base class for all other generic views. """ + model = None serializer_class = None model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS @@ -30,8 +31,10 @@ class GenericAPIView(views.APIView): def get_serializer_class(self): """ Return the class to use for the serializer. - Use `self.serializer_class`, falling back to constructing a - model serializer class from `self.model_serializer_class` + + Defaults to using `self.serializer_class`, falls back to constructing a + model serializer class using `self.model_serializer_class`, with + `self.model` as the model. """ serializer_class = self.serializer_class @@ -58,29 +61,42 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS paginate_by = api_settings.PAGINATE_BY + paginate_by_param = api_settings.PAGINATE_BY_PARAM filter_backend = api_settings.FILTER_BACKEND def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + """ if not self.filter_backend: return queryset backend = self.filter_backend() return backend.filter_queryset(self.request, queryset, self) - def get_pagination_serializer_class(self): + def get_pagination_serializer(self, page=None): """ - Return the class to use for the pagination serializer. + Return a serializer instance to use with paginated data. """ class SerializerClass(self.pagination_serializer_class): class Meta: object_serializer_class = self.get_serializer_class() - return SerializerClass - - def get_pagination_serializer(self, page=None): - pagination_serializer_class = self.get_pagination_serializer_class() + pagination_serializer_class = SerializerClass context = self.get_serializer_context() return pagination_serializer_class(instance=page, context=context) + def get_paginate_by(self, queryset): + """ + Return the size of pages to use with pagination. + """ + if self.paginate_by_param: + params = self.request.QUERY_PARAMS + try: + return int(params[self.paginate_by_param]) + except (KeyError, ValueError): + pass + return self.paginate_by + class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): """ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 0da4c2cc6..53c4d9842 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -7,7 +7,6 @@ which allows mixin classes to be composed in interesting ways. from django.http import Http404 from rest_framework import status from rest_framework.response import Response -from rest_framework.settings import api_settings class CreateModelMixin(object): @@ -40,7 +39,6 @@ class ListModelMixin(object): Should be mixed in with `MultipleObjectAPIView`. """ empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." - page_size_kwarg = api_settings.PAGE_SIZE_KWARG def list(self, request, *args, **kwargs): queryset = self.get_queryset() @@ -66,17 +64,6 @@ class ListModelMixin(object): return Response(serializer.data) - def get_paginate_by(self, queryset): - if self.page_size_kwarg is not None: - page_size_kwarg = self.request.QUERY_PARAMS.get(self.page_size_kwarg) - if page_size_kwarg: - try: - page_size = int(page_size_kwarg) - return page_size - except ValueError: - pass - return super(ListModelMixin, self).get_paginate_by(queryset) - class RetrieveModelMixin(object): """ diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 8883b9637..ee24a4ad9 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -54,12 +54,19 @@ DEFAULTS = { 'user': None, 'anon': None, }, + + # Pagination 'PAGINATE_BY': None, + 'PAGINATE_BY_PARAM': None, + + # Filtering 'FILTER_BACKEND': None, + # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # Browser enhancements 'FORM_METHOD_OVERRIDE': '_method', 'FORM_CONTENT_OVERRIDE': '_content', 'FORM_CONTENTTYPE_OVERRIDE': '_content_type', @@ -67,8 +74,6 @@ DEFAULTS = { 'URL_FORMAT_OVERRIDE': 'format', 'FORMAT_SUFFIX_KWARG': 'format', - - 'PAGE_SIZE_KWARG': 'page_size' } diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 8aae21471..3062007d4 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -36,25 +36,17 @@ if django_filters: class DefaultPageSizeKwargView(generics.ListAPIView): """ - View for testing default page_size usage + View for testing default paginate_by_param usage """ model = BasicModel -class CustomPageSizeKwargView(generics.ListAPIView): +class PaginateByParamView(generics.ListAPIView): """ - View for testing custom page_size usage + View for testing custom paginate_by_param usage """ model = BasicModel - page_size_kwarg = 'ps' - - -class NonePageSizeKwargView(generics.ListAPIView): - """ - View for testing None page_size usage - """ - model = BasicModel - page_size_kwarg = None + paginate_by_param = 'page_size' class IntegrationTestPagination(TestCase): @@ -181,9 +173,9 @@ class UnitTestPagination(TestCase): self.assertEquals(serializer.data['results'], self.objects[20:]) -class TestDefaultPageSizeKwarg(TestCase): +class TestUnpaginated(TestCase): """ - Tests for list views with default page size kwarg + Tests for list views without pagination. """ def setUp(self): @@ -199,26 +191,17 @@ class TestDefaultPageSizeKwarg(TestCase): ] self.view = DefaultPageSizeKwargView.as_view() - def test_default_page_size(self): + def test_unpaginated(self): """ Tests the default page size for this view. no page size --> no limit --> no meta data """ request = factory.get('/') - response = self.view(request).render() + response = self.view(request) self.assertEquals(response.data, self.data) - def test_default_page_size_kwarg(self): - """ - If page_size_kwarg is set not set, the default page_size kwarg should limit per view requests. - """ - request = factory.get('/?page_size=5') - response = self.view(request).render() - self.assertEquals(response.data['count'], 13) - self.assertEquals(response.data['results'], self.data[:5]) - -class TestCustomPageSizeKwarg(TestCase): +class TestCustomPaginateByParam(TestCase): """ Tests for list views with default page size kwarg """ @@ -234,7 +217,7 @@ class TestCustomPageSizeKwarg(TestCase): {'id': obj.id, 'text': obj.text} for obj in self.objects.all() ] - self.view = CustomPageSizeKwargView.as_view() + self.view = PaginateByParamView.as_view() def test_default_page_size(self): """ @@ -245,55 +228,11 @@ class TestCustomPageSizeKwarg(TestCase): response = self.view(request).render() self.assertEquals(response.data, self.data) - def test_disabled_default_page_size_kwarg(self): + def test_paginate_by_param(self): """ - If page_size_kwarg is set set, the default page_size kwarg should not work. + If paginate_by_param is set, the new kwarg should limit per view requests. """ request = factory.get('/?page_size=5') response = self.view(request).render() - self.assertEquals(response.data, self.data) - - def test_custom_page_size_kwarg(self): - """ - If page_size_kwarg is set set, the new kwarg should limit per view requests. - """ - request = factory.get('/?ps=5') - response = self.view(request).render() self.assertEquals(response.data['count'], 13) self.assertEquals(response.data['results'], self.data[:5]) - - -class TestNonePageSizeKwarg(TestCase): - """ - Tests for list views with default page size kwarg - """ - - def setUp(self): - """ - Create 13 BasicModel instances. - """ - for i in range(13): - BasicModel(text=i).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = NonePageSizeKwargView.as_view() - - def test_default_page_size(self): - """ - Tests the default page size for this view. - no page size --> no limit --> no meta data - """ - request = factory.get('/') - response = self.view(request).render() - self.assertEquals(response.data, self.data) - - def test_none_page_size_kwarg(self): - """ - If page_size_kwarg is set to None, custom page_size per request should be disabled. - """ - request = factory.get('/?page_size=5') - response = self.view(request).render() - self.assertEquals(response.data, self.data) From 19b0516bfefe3398683b4f878774e3dd80bf653a Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 22:49:15 +0000 Subject: [PATCH 48/79] Getting 2.1.3 release notes ready --- README.md | 8 ++++++++ docs/topics/release-notes.md | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3ffc9a7d..d03fc80ef 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ To run the tests. # Changelog +## 2.1.3 + +**Date**: 16th Nov 2012 + +* 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. + ## 2.1.2 **Date**: 9th Nov 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 869cabc89..5931a75ad 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,7 +4,9 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -## Master +## 2.1.3 + +**Date**: 16th Nov 2012 * 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. From 016ef5019ff43808540f948d674e8dd33247cb99 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 22:58:17 +0000 Subject: [PATCH 49/79] Version 2.1.3 --- README.md | 3 +++ docs/topics/release-notes.md | 3 +++ rest_framework/__init__.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d03fc80ef..9a12d5358 100644 --- a/README.md +++ b/README.md @@ -62,9 +62,12 @@ To run the tests. **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 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 5931a75ad..2a6fbe839 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -8,9 +8,12 @@ **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 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fd176603c..88108a8d2 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.2' +__version__ = '2.1.3' VERSION = __version__ # synonym From acbe991209ed9112af80db99d832704641276844 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 23:22:15 +0000 Subject: [PATCH 50/79] Tidying --- rest_framework/generics.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index be225d0ab..dd8dfcf8d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -14,6 +14,7 @@ class GenericAPIView(views.APIView): """ Base class for all other generic views. """ + model = None serializer_class = None model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS @@ -47,7 +48,10 @@ class GenericAPIView(views.APIView): return serializer_class def get_serializer(self, instance=None, data=None, files=None): - # TODO: add support for seperate serializer/deserializer + """ + Return the serializer instance that should be used for validating and + deserializing input, and for serializing output. + """ serializer_class = self.get_serializer_class() context = self.get_serializer_context() return serializer_class(instance, data=data, files=files, context=context) @@ -58,9 +62,9 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): Base class for generic views onto a queryset. """ - pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS filter_backend = api_settings.FILTER_BACKEND def filter_queryset(self, queryset): @@ -89,9 +93,9 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): Return the size of pages to use with pagination. """ if self.paginate_by_param: - params = self.request.QUERY_PARAMS + query_params = self.request.QUERY_PARAMS try: - return int(params[self.paginate_by_param]) + return int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass return self.paginate_by @@ -101,8 +105,10 @@ class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): """ Base class for generic views onto a model instance. """ + pk_url_kwarg = 'pk' # Not provided in Django 1.3 slug_url_kwarg = 'slug' # Not provided in Django 1.3 + slug_field = 'slug' def get_object(self, queryset=None): """ From 0eba278e1391604086dab1dfa1bd6ea86fea282e Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 23:22:23 +0000 Subject: [PATCH 51/79] Improve pagination docs --- docs/api-guide/pagination.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 597baba4d..39e6a32d2 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -80,23 +80,21 @@ We could now use our pagination serializer in a view like this. ## Pagination in the generic views -The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, or by turning pagination off completely. +The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely. -The default pagination style may be set globally, using the `PAGINATION_SERIALIZER` and `PAGINATE_BY` settings. For example. +The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example. REST_FRAMEWORK = { - 'PAGINATION_SERIALIZER': ( - 'example_app.pagination.CustomPaginationSerializer', - ), - 'PAGINATE_BY': 10 + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size' } You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view. class PaginatedListView(ListAPIView): model = ExampleModel - pagination_serializer_class = CustomPaginationSerializer paginate_by = 10 + paginate_by_param = 'page_size' For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. @@ -122,4 +120,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam results_field = 'objects' +## Using your custom pagination serializer + +To have your custom pagination serializer be used by default use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting: + + REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_SERIALIZER_CLASS': + 'example_app.pagination.CustomPaginationSerializer', + } + +Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view: + + class PaginatedListView(ListAPIView): + model = ExampleModel + pagination_serializer_class = CustomPaginationSerializer + paginate_by = 10 + [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ From 2263ed8b9409c709f6dbad2157f8debffb16c1d8 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Fri, 16 Nov 2012 23:24:36 +0000 Subject: [PATCH 52/79] Tweak --- 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 39e6a32d2..5a35ed756 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -122,7 +122,7 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam ## Using your custom pagination serializer -To have your custom pagination serializer be used by default use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting: +To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting: REST_FRAMEWORK = { 'DEFAULT_PAGINATION_SERIALIZER_CLASS': From 4068323df4a8a8ad8825d5e0ed1d31ee2a36484f Mon Sep 17 00:00:00 2001 From: Eugene MechanisM <eugene@mechanism.name> Date: Sat, 17 Nov 2012 04:03:43 +0400 Subject: [PATCH 53/79] Missing import of "Permission" model in docs Missing import of "Permission" model in docs --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 93da1a594..9a36a2b0d 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`. Once you' First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations. - from django.contrib.auth.models import User, Group + from django.contrib.auth.models import User, Group, Permission from rest_framework import serializers From 346a79b170b0a25fd28354de765c5aa5aca9a119 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Sat, 17 Nov 2012 00:29:15 +0000 Subject: [PATCH 54/79] Added @MechanisM - Thanks! (That's a mighty kick ass Gravatar) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 8e71c937a..f037e816d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -60,6 +60,7 @@ The following people have helped make REST framework great. * Ben Konrath - [benkonrath] * Marc Aymerich - [glic3rinu] * Ludwig Kraatz - [ludwigkraatz] +* Eugene Mechanism - [mechanism] Many thanks to everyone who's contributed to the project. @@ -155,3 +156,4 @@ To contact the author directly: [benkonrath]: https://github.com/benkonrath [glic3rinu]: https://github.com/glic3rinu [ludwigkraatz]: https://github.com/ludwigkraatz +[mechanism]: https://github.com/mechanism From f0d4232c1de2c3154535312d1d48c59854ffa162 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 17:46:16 +0100 Subject: [PATCH 55/79] Django 1.5 support, and awareness for AUTH_USER_MODEL --- rest_framework/authtoken/models.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 5b3071aa7..610de8d8d 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -2,14 +2,28 @@ import uuid import hmac from hashlib import sha1 from django.db import models +from django import VERSION +try: + from django.db.models.auth import User + user_model = User + except ImportError: + raise ImportError + else: + raise + +if VERSION[:2] in ((1, 5,),): + from django.conf import settings + if hasattr(settings, AUTH_USER_MODEL): + user_model = settings.AUTH_USER_MODEL + class Token(models.Model): """ The default authorization token model. """ key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField('auth.User', related_name='auth_token') + user = models.OneToOneField(user_model, related_name='auth_token') created = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): From 3c1b5c34356eef4ff1a2ecdec26c761c7a27eb27 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 17:53:08 +0100 Subject: [PATCH 56/79] indent error --- rest_framework/authtoken/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 610de8d8d..08b70f529 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -6,11 +6,11 @@ from django import VERSION try: from django.db.models.auth import User - user_model = User - except ImportError: - raise ImportError - else: - raise + user_model = User +except ImportError: + raise ImportError +else: + raise if VERSION[:2] in ((1, 5,),): from django.conf import settings From bbb5a8a1d90573665c18b70add60d12e8a36882f Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 18:01:46 +0100 Subject: [PATCH 57/79] fixed import error --- rest_framework/authtoken/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 08b70f529..fa1f4ea61 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -4,18 +4,18 @@ from hashlib import sha1 from django.db import models from django import VERSION -try: - from django.db.models.auth import User - user_model = User -except ImportError: - raise ImportError -else: - raise if VERSION[:2] in ((1, 5,),): from django.conf import settings if hasattr(settings, AUTH_USER_MODEL): user_model = settings.AUTH_USER_MODEL + else: + from django.contrib.auth.models import User as user_model +else: + try: + from django.db.models.auth import User as user_model + except ImportError: + raise ImportError('User model is not to be found.') class Token(models.Model): From cd482c0ad22bbb810378c61e02a790e5e3796aa7 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 18:04:37 +0100 Subject: [PATCH 58/79] Added support for Django 1.5 for TokenAuth --- rest_framework/authtoken/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index fa1f4ea61..3b8bffeb8 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -7,7 +7,7 @@ from django import VERSION if VERSION[:2] in ((1, 5,),): from django.conf import settings - if hasattr(settings, AUTH_USER_MODEL): + if hasattr(settings, 'AUTH_USER_MODEL'): user_model = settings.AUTH_USER_MODEL else: from django.contrib.auth.models import User as user_model From 8eb4bb8090a84282c3537641e8ecb5c38a33fc41 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 20:35:15 +0100 Subject: [PATCH 59/79] Moved function for getting correct user model to compat.py --- rest_framework/authtoken/models.py | 17 ++--------------- rest_framework/compat.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 3b8bffeb8..4da2aa625 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,29 +1,16 @@ import uuid import hmac from hashlib import sha1 +from rest_framework.compat import User from django.db import models -from django import VERSION -if VERSION[:2] in ((1, 5,),): - from django.conf import settings - if hasattr(settings, 'AUTH_USER_MODEL'): - user_model = settings.AUTH_USER_MODEL - else: - from django.contrib.auth.models import User as user_model -else: - try: - from django.db.models.auth import User as user_model - except ImportError: - raise ImportError('User model is not to be found.') - - class Token(models.Model): """ The default authorization token model. """ key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField(user_model, related_name='auth_token') + user = models.OneToOneField(User, related_name='auth_token') created = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): diff --git a/rest_framework/compat.py b/rest_framework/compat.py index e38e7c33a..d5ad2a7aa 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -27,6 +27,20 @@ def get_concrete_model(model_cls): return model_cls +# Django 1.5 add support for custom auth user model +if django.VERSION >= (1, 5): + from django.conf import settings + if hasattr(settings, 'AUTH_USER_MODEL'): + User = settings.AUTH_USER_MODEL + else: + from django.contrib.auth.models import User +else: + try: + from django.db.models.auth import User + except ImportError: + raise ImportError('User model is not to be found.') + + # First implementation of Django class-based views did not include head method # in base View class - https://code.djangoproject.com/ticket/15668 if django.VERSION >= (1, 4): From 9f378d0dd44789cb98f3409604d9130d7a0032a8 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sat, 17 Nov 2012 23:51:05 +0100 Subject: [PATCH 60/79] fixed bug --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d5ad2a7aa..2f59be95a 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -36,7 +36,7 @@ if django.VERSION >= (1, 5): from django.contrib.auth.models import User else: try: - from django.db.models.auth import User + from django.contrib.auth.models import User except ImportError: raise ImportError('User model is not to be found.') From d67ee708e5d9f28f26377df391f5e72708e073d2 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson <m@jacobian.se> Date: Sun, 18 Nov 2012 18:14:21 +0100 Subject: [PATCH 61/79] Add support for min_length / max_length keywords on basic ModelFields --- rest_framework/fields.py | 11 +++++++++++ rest_framework/tests/models.py | 8 ++++++++ rest_framework/tests/serializer.py | 10 ++++++++++ 3 files changed, 29 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c68c39b59..01cf5ae3d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -215,8 +215,19 @@ class ModelField(WritableField): self.model_field = kwargs.pop('model_field') except: raise ValueError("ModelField requires 'model_field' kwarg") + + self.min_length = kwargs.pop('min_length', + getattr(self.model_field, 'min_length', None)) + self.max_length = kwargs.pop('max_length', + getattr(self.model_field, 'max_length', None)) + super(ModelField, self).__init__(*args, **kwargs) + if self.min_length is not None: + self.validators.append(validators.MinLengthValidator(self.min_length)) + if self.max_length is not None: + self.validators.append(validators.MaxLengthValidator(self.max_length)) + def from_native(self, value): rel = getattr(self.model_field, "rel", None) if rel is not None: diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index cbdc765c8..59d811502 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -35,6 +35,13 @@ def foobar(): return 'foobar' +class CustomField(models.CharField): + + def __init__(self, *args, **kwargs): + kwargs['max_length'] = 12 + super(CustomField, self).__init__(*args, **kwargs) + + class RESTFrameworkModel(models.Model): """ Base for test models that sets app_label, so they play nicely. @@ -113,6 +120,7 @@ class Comment(RESTFrameworkModel): class ActionItem(RESTFrameworkModel): title = models.CharField(max_length=200) done = models.BooleanField(default=False) + info = CustomField(default='---', max_length=12) # Models for reverse relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index fb1be7eb0..814c24993 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -41,6 +41,7 @@ class CommentSerializer(serializers.Serializer): class ActionItemSerializer(serializers.ModelSerializer): + class Meta: model = ActionItem @@ -247,6 +248,15 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'title': [u'Ensure this value has at most 200 characters (it has 201).']}) + def test_default_modelfield_max_length_exceeded(self): + data = { + 'title': 'Testing "info" field...', + 'info': 'x' * 13, + } + serializer = ActionItemSerializer(data=data) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) + class MetadataTests(TestCase): def test_empty(self): From f131e533edf58dc8ba7b712b4c3486a3ab053ffc Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Sun, 18 Nov 2012 17:57:02 +0000 Subject: [PATCH 62/79] Docs, docs, docs, docs, docs, docs --- docs/api-guide/generic-views.md | 18 ++++++++++++++++++ docs/api-guide/pagination.md | 5 +++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 33ec89d28..428323b89 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -163,30 +163,48 @@ The mixin classes provide the actions that are used to provide the basic view be Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. +If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. + +If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`. + Should be mixed in with [MultipleObjectAPIView]. ## CreateModelMixin Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. +If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value. + +If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. + Should be mixed in with any [GenericAPIView]. ## RetrieveModelMixin Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. +If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`. + Should be mixed in with [SingleObjectAPIView]. ## UpdateModelMixin Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. +If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response. + +If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response. + +If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. + Should be mixed in with [SingleObjectAPIView]. ## DestroyModelMixin Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance. +If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`. + Should be mixed in with [SingleObjectAPIView]. [cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 5a35ed756..ab335e6e2 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -70,11 +70,12 @@ We could now use our pagination serializer in a view like this. # If page is not an integer, deliver first page. users = paginator.page(1) except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. + # If page is out of range (e.g. 9999), + # deliver last page of results. users = paginator.page(paginator.num_pages) serializer_context = {'request': request} - serializer = PaginatedUserSerializer(instance=users, + serializer = PaginatedUserSerializer(users, context=serializer_context) return Response(serializer.data) From 91c0249c9d622670252030cb36ea872c08d91471 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas@Jonass-MacBook-Pro.local> Date: Sun, 18 Nov 2012 21:12:06 +0100 Subject: [PATCH 63/79] fixed migration to support django 1.5 --- rest_framework/authtoken/migrations/0001_initial.py | 3 ++- rest_framework/compat.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 9d750381d..2c0c40b10 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -3,6 +3,7 @@ import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models +from rest_framework.compat import User class Migration(SchemaMigration): @@ -11,7 +12,7 @@ class Migration(SchemaMigration): # Adding model 'Token' db.create_table('authtoken_token', ( ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['auth.User'])), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal('authtoken', ['Token']) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 2f59be95a..09b763681 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -38,7 +38,7 @@ else: try: from django.contrib.auth.models import User except ImportError: - raise ImportError('User model is not to be found.') + raise ImportError(u"User model is not to be found.") # First implementation of Django class-based views did not include head method From b03804fe05225d22c14471a18b96197fdf31dce9 Mon Sep 17 00:00:00 2001 From: glic3rinu <glic3rinu@e4200.(none)> Date: Mon, 19 Nov 2012 00:14:03 +0100 Subject: [PATCH 64/79] Fixed identation on filter_fields --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index ccae48250..bcc876607 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -45,7 +45,7 @@ class DjangoFilterBackend(BaseFilterBackend): class AutoFilterSet(self.default_filter_set): class Meta: model = view_model - fields = filter_fields + fields = filter_fields return AutoFilterSet return None From 0ce792eac2f234d3858ff9e892845715b7460a4f Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas.liljestrand@gmail.com> Date: Mon, 19 Nov 2012 11:19:45 +0100 Subject: [PATCH 65/79] Update rest_framework/authtoken/migrations/0001_initial.py revert migration --- rest_framework/authtoken/migrations/0001_initial.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 2c0c40b10..df6749fbb 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -3,7 +3,6 @@ import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models -from rest_framework.compat import User class Migration(SchemaMigration): @@ -12,7 +11,7 @@ class Migration(SchemaMigration): # Adding model 'Token' db.create_table('authtoken_token', ( ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm["auth.User"])), ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal('authtoken', ['Token']) From 0bcc840927ab08d0e7d64844f3242036a142113d Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand <jonas.liljestrand@area59.se> Date: Mon, 19 Nov 2012 11:37:37 +0100 Subject: [PATCH 66/79] Complete fix for migration --- .../authtoken/migrations/0001_initial.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 2c0c40b10..f4e052e48 100644 --- a/rest_framework/authtoken/migrations/0001_initial.py +++ b/rest_framework/authtoken/migrations/0001_initial.py @@ -3,7 +3,14 @@ import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models -from rest_framework.compat import User + + +try: + from django.contrib.auth import get_user_model +except ImportError: # django < 1.5 + from django.contrib.auth.models import User +else: + User = get_user_model() class Migration(SchemaMigration): @@ -12,7 +19,7 @@ class Migration(SchemaMigration): # Adding model 'Token' db.create_table('authtoken_token', ( ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), - ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm["%s.%s" % (User._meta.app_label, User._meta.object_name)])), + ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])), ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal('authtoken', ['Token']) @@ -37,7 +44,7 @@ class Migration(SchemaMigration): 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, - 'auth.user': { + "%s.%s" % (User._meta.app_label, User._meta.module_name): { 'Meta': {'object_name': 'User'}, 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), @@ -57,7 +64,7 @@ class Migration(SchemaMigration): 'Meta': {'object_name': 'Token'}, 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), - 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['auth.User']"}) + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)}) }, 'contenttypes.contenttype': { 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, From 728e505180d130192c54eec47bd191ab459ebf83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Mon, 19 Nov 2012 17:35:28 +0100 Subject: [PATCH 67/79] updated to buildin status codes --- rest_framework/renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 870464f0b..a2b7704dd 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -19,7 +19,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 +from rest_framework import VERSION, status from rest_framework import serializers, parsers @@ -479,7 +479,7 @@ class BrowsableAPIRenderer(BaseRenderer): # Munge DELETE Response code to allow us to return content # (Do this *after* we've rendered the template so that we include # the normal deletion response code in the output) - if response.status_code == 204: - response.status_code = 200 + if response.status_code == status.HTTP_204_NO_CONTENT: + response.status_code = status.HTTP_200_OK return ret From de5b071d677074ab3b6b33a843c4b05ba2052a6b Mon Sep 17 00:00:00 2001 From: Jamie Matthews <jamie.matthews@gmail.com> Date: Mon, 19 Nov 2012 17:22:17 +0000 Subject: [PATCH 68/79] Add SerializerMethodField --- docs/api-guide/fields.md | 6 ++++++ rest_framework/fields.py | 14 ++++++++++++ rest_framework/tests/serializer.py | 34 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d1c31ecc7..b19c324ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -324,5 +324,11 @@ This field is always read-only. * `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. * `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. +# Other Fields + +## SerializerMethodField + +This is a read-only field gets its value by calling a method on the serializer class it's attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. + [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c68c39b59..d1e9c45d8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1019,3 +1019,17 @@ class ImageField(FileField): if hasattr(f, 'seek') and callable(f.seek): f.seek(0) return f + + +class SerializerMethodField(Field): + """ + A field that gets its value by calling a method on the serializer it's attached to. + """ + + def __init__(self, method_name): + self.method_name = method_name + super(SerializerMethodField, self).__init__() + + def field_to_native(self, obj, field_name): + value = getattr(self.parent, self.method_name)(obj) + return self.to_native(value) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index fb1be7eb0..cc6e9d5cf 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -497,6 +497,40 @@ class ManyRelatedTests(TestCase): self.assertEqual(serializer.data, expected) +class SerializerMethodFieldTests(TestCase): + def setUp(self): + + class BoopSerializer(serializers.Serializer): + beep = serializers.SerializerMethodField('get_beep') + boop = serializers.Field() + boop_count = serializers.SerializerMethodField('get_boop_count') + + def get_beep(self, obj): + return 'hello!' + + def get_boop_count(self, obj): + return len(obj.boop) + + self.serializer_class = BoopSerializer + + def test_serializer_method_field(self): + + class MyModel(object): + boop = ['a', 'b', 'c'] + + source_data = MyModel() + + serializer = self.serializer_class(source_data) + + expected = { + 'beep': u'hello!', + 'boop': [u'a', u'b', u'c'], + 'boop_count': 3, + } + + self.assertEqual(serializer.data, expected) + + # Test for issue #324 class BlankFieldTests(TestCase): def setUp(self): From 3ab8c4966d065e930bd6e8bc6c26934ae5c5918c Mon Sep 17 00:00:00 2001 From: Jamie Matthews <jamie.matthews@gmail.com> Date: Mon, 19 Nov 2012 17:24:08 +0000 Subject: [PATCH 69/79] Tweaks to SerializerMethodField docs --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b19c324ad..ebfb5d47d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -328,7 +328,7 @@ This field is always read-only. ## SerializerMethodField -This is a read-only field gets its value by calling a method on the serializer class it's attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. +This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS From ce5b186ca869b693c945200581ba893123a63ce8 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Mon, 19 Nov 2012 21:42:33 +0000 Subject: [PATCH 70/79] Docs tweaks. --- docs/api-guide/authentication.md | 21 ++++++++++++--------- docs/topics/release-notes.md | 4 +++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index a30bd22c1..05575f573 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -68,7 +68,7 @@ This policy uses [HTTP Basic Authentication][basicauth], signed against a user's If successfully authenticated, `BasicAuthentication` provides the following credentials. -* `request.user` will be a `django.contrib.auth.models.User` instance. +* `request.user` will be a Django `User` instance. * `request.auth` will be `None`. **Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage. @@ -92,7 +92,7 @@ For clients to authenticate, the token key should be included in the `Authorizat If successfully authenticated, `TokenAuthentication` provides the following credentials. -* `request.user` will be a `django.contrib.auth.models.User` instance. +* `request.user` will be a Django `User` instance. * `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance. **Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only. @@ -104,7 +104,7 @@ If you want every user to have an automatically generated Token, you can simply if created: Token.objects.create(user=instance) -If you've already created some User`'s, you can run a script like this. +If you've already created some User's, you can run a script like this. from django.contrib.auth.models import User from rest_framework.authtoken.models import Token @@ -112,26 +112,29 @@ If you've already created some User`'s, you can run a script like this. for user in User.objects.all(): Token.objects.get_or_create(user=user) -When using TokenAuthentication, it may be useful to add a login view for clients to retrieve the token. - -REST framework provides a built-in login view for clients to retrieve the token called `rest_framework.authtoken.obtain_auth_token`. To use it, add a pattern to include the token login view for clients as follows: +When using TokenAuthentication, you may want to provide a mechanism for clients to obtain a token, given the username and password. +REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf: urlpatterns += patterns('', url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token') ) -The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. The authtoken login view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using forms or JSON: +The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. + +The `obtain_auth_token` view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using form data or JSON: { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } +<!-- ## OAuthAuthentication This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf. If successfully authenticated, `OAuthAuthentication` provides the following credentials. -* `request.user` will be a `django.contrib.auth.models.User` instance. +* `request.user` will be a Django `User` instance. * `request.auth` will be a `rest_framework.models.OAuthToken` instance. +--> ## SessionAuthentication @@ -139,7 +142,7 @@ This policy uses Django's default session backend for authentication. Session a If successfully authenticated, `SessionAuthentication` provides the following credentials. -* `request.user` will be a `django.contrib.auth.models.User` instance. +* `request.user` will be a Django `User` instance. * `request.auth` will be `None`. # Custom authentication diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d43f892f7..81f4e332f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,7 +4,9 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -* Add convenience login view to get tokens when using `TokenAuthentication` +## Master + +* Added `obtain_token_view` to get tokens when using `TokenAuthentication` ## 2.1.3 From 83f3770af12061185289ac9e4b9c0a24e82f10c3 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Mon, 19 Nov 2012 21:47:34 +0000 Subject: [PATCH 71/79] Adding folks to the credits makes me happy. Good work @jonlil! --- docs/topics/credits.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 34590109b..bd9e4f487 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -62,6 +62,7 @@ The following people have helped make REST framework great. * Ludwig Kraatz - [ludwigkraatz] * Rob Romano - [robromano] * Eugene Mechanism - [mechanism] +* Jonas Liljestrand - [jonlil] Many thanks to everyone who's contributed to the project. @@ -159,4 +160,4 @@ To contact the author directly: [ludwigkraatz]: https://github.com/ludwigkraatz [robromano]: https://github.com/robromano [mechanism]: https://github.com/mechanism - +[jonlil]: https://github.com/jonlil From 588e4dda6d97fad08b39e65df1ef25e643261977 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Mon, 19 Nov 2012 21:49:07 +0000 Subject: [PATCH 72/79] Added 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 81f4e332f..ec83387f5 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -7,6 +7,7 @@ ## Master * Added `obtain_token_view` to get tokens when using `TokenAuthentication` +* Bugfix: Django 1.5 configurable user support for `TokenAuthentication` ## 2.1.3 From a44a94dd6ea2d9497264e267a0354cb684d398f6 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Mon, 19 Nov 2012 22:08:38 +0000 Subject: [PATCH 73/79] More docs tweaking. --- docs/api-guide/authentication.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 05575f573..8ed6ef318 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -104,7 +104,7 @@ If you want every user to have an automatically generated Token, you can simply if created: Token.objects.create(user=instance) -If you've already created some User's, you can run a script like this. +If you've already created some users, you can generate tokens for all existing users like this: from django.contrib.auth.models import User from rest_framework.authtoken.models import Token @@ -112,16 +112,16 @@ If you've already created some User's, you can run a script like this. for user in User.objects.all(): Token.objects.get_or_create(user=user) -When using TokenAuthentication, you may want to provide a mechanism for clients to obtain a token, given the username and password. +When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf: urlpatterns += patterns('', url(r'^api-token-auth/', 'rest_framework.authtoken.obtain_auth_token') ) -The `r'^api-token-auth/'` part of pattern can actually be whatever URL you want to use. +Note that the URL part of the pattern can be whatever you want to use. -The `obtain_auth_token` view will render a JSON response when a valid `username` and `password` fields are POST'ed to the view using form data or JSON: +The `obtain_auth_token` view will return a JSON response when valid `username` and `password` fields are POSTed to the view using form data or JSON: { 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' } From 2cf0fda2ae5cf596946df77675ce10d68587a8bd Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Mon, 19 Nov 2012 22:09:40 -0800 Subject: [PATCH 74/79] Cache default fields per serializer instance for improved performance --- rest_framework/serializers.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 397866a76..46ffc0494 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -103,6 +103,7 @@ class BaseSerializer(Field): self.init_data = data self.init_files = files self.object = instance + self.default_fields = self.get_default_fields() self._data = None self._files = None @@ -111,18 +112,18 @@ class BaseSerializer(Field): ##### # Methods to determine which fields to use when (de)serializing objects. - def default_fields(self, nested=False): + def get_default_fields(self): """ Return the complete set of default fields for the object, as a dict. """ return {} - def get_fields(self, nested=False): + def get_fields(self): """ Returns the complete set of fields for the object as a dict. This will be the set of any explicitly declared fields, - plus the set of fields returned by default_fields(). + plus the set of fields returned by get_default_fields(). """ ret = SortedDict() @@ -133,8 +134,7 @@ class BaseSerializer(Field): field.initialize(parent=self, field_name=key) # Add in the default fields - fields = self.default_fields(nested) - for key, val in fields.items(): + for key, val in self.default_fields.items(): if key not in ret: ret[key] = val @@ -181,7 +181,7 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() for field_name, field in fields.items(): key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) @@ -194,7 +194,7 @@ class BaseSerializer(Field): Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. """ - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() reverted_data = {} for field_name, field in fields.items(): try: @@ -209,7 +209,7 @@ class BaseSerializer(Field): Run `validate_<fieldname>()` and `validate()` methods on the serializer """ # TODO: refactor this so we're not determining the fields again - fields = self.get_fields(nested=bool(self.opts.depth)) + fields = self.get_fields() for field_name, field in fields.items(): try: @@ -332,16 +332,10 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - def default_fields(self, nested=False): + def get_default_fields(self): """ Return all the fields that should be serialized for the model. """ - # TODO: Modify this so that it's called on init, and drop - # serialize/obj/data arguments. - # - # We *could* provide a hook for dynamic fields, but - # it'd be nice if the default was to generate fields statically - # at the point of __init__ cls = self.opts.model opts = get_concrete_model(cls)._meta @@ -353,6 +347,7 @@ class ModelSerializer(Serializer): fields += [field for field in opts.many_to_many if field.serialize] ret = SortedDict() + nested = bool(self.opts.depth) is_pk = True # First field in the list is the pk for model_field in fields: From 68c397371c647e88270b8c9e9a6f5f610bbd3a2b Mon Sep 17 00:00:00 2001 From: Jamie Matthews <jamie.matthews@gmail.com> Date: Tue, 20 Nov 2012 09:41:36 +0000 Subject: [PATCH 75/79] Fix related serializers with source argument that resolves to a callable --- rest_framework/serializers.py | 3 +++ rest_framework/tests/models.py | 3 +++ rest_framework/tests/serializer.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 397866a76..2e7e2cf5d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -277,6 +277,9 @@ class BaseSerializer(Field): """ obj = getattr(obj, self.source or field_name) + if is_simple_callable(obj): + obj = obj() + # If the object has an "all" method, assume it's a relationship if is_simple_callable(getattr(obj, 'all', None)): return [self.to_native(item) for item in obj.all()] diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 59d811502..3704cda7b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -127,6 +127,9 @@ class ActionItem(RESTFrameworkModel): class BlogPost(RESTFrameworkModel): title = models.CharField(max_length=100) + def get_first_comment(self): + return self.blogpostcomment_set.all()[0] + class BlogPostComment(RESTFrameworkModel): text = models.TextField() diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 814c24993..9ca4f002f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -488,6 +488,7 @@ class ManyRelatedTests(TestCase): title = serializers.CharField() comments = BlogPostCommentSerializer(source='blogpostcomment_set') + self.comment_serializer_class = BlogPostCommentSerializer self.serializer_class = BlogPostSerializer def test_reverse_relations(self): @@ -506,6 +507,23 @@ class ManyRelatedTests(TestCase): self.assertEqual(serializer.data, expected) + def test_callable_source(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class ExtendedBlogPostSerializer(self.serializer_class): + first_comment = self.comment_serializer_class(source='get_first_comment') + + serializer = ExtendedBlogPostSerializer(post) + expected = { + 'title': 'Test blog post', + 'comments': [ + {'text': 'I love this blog post'} + ], + 'first_comment': {'text': 'I love this blog post'} + } + self.assertEqual(serializer.data, expected) + # Test for issue #324 class BlankFieldTests(TestCase): From 3cc5349b2f98d9c70788a2aadefc150290316479 Mon Sep 17 00:00:00 2001 From: Jamie Matthews <jamie.matthews@gmail.com> Date: Tue, 20 Nov 2012 09:49:54 +0000 Subject: [PATCH 76/79] Clean up and clarify tests for related serializers --- rest_framework/tests/serializer.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 9ca4f002f..d522ef972 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -479,7 +479,10 @@ class CallableDefaultValueTests(TestCase): class ManyRelatedTests(TestCase): - def setUp(self): + def test_reverse_relations(self): + post = BlogPost.objects.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") class BlogPostCommentSerializer(serializers.Serializer): text = serializers.CharField() @@ -488,15 +491,7 @@ class ManyRelatedTests(TestCase): title = serializers.CharField() comments = BlogPostCommentSerializer(source='blogpostcomment_set') - self.comment_serializer_class = BlogPostCommentSerializer - self.serializer_class = BlogPostSerializer - - def test_reverse_relations(self): - post = BlogPost.objects.create(title="Test blog post") - post.blogpostcomment_set.create(text="I hate this blog post") - post.blogpostcomment_set.create(text="I love this blog post") - - serializer = self.serializer_class(instance=post) + serializer = BlogPostSerializer(instance=post) expected = { 'title': 'Test blog post', 'comments': [ @@ -511,15 +506,17 @@ class ManyRelatedTests(TestCase): post = BlogPost.objects.create(title="Test blog post") post.blogpostcomment_set.create(text="I love this blog post") - class ExtendedBlogPostSerializer(self.serializer_class): - first_comment = self.comment_serializer_class(source='get_first_comment') + class BlogPostCommentSerializer(serializers.Serializer): + text = serializers.CharField() + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + first_comment = BlogPostCommentSerializer(source='get_first_comment') + + serializer = BlogPostSerializer(post) - serializer = ExtendedBlogPostSerializer(post) expected = { 'title': 'Test blog post', - 'comments': [ - {'text': 'I love this blog post'} - ], 'first_comment': {'text': 'I love this blog post'} } self.assertEqual(serializer.data, expected) From 5f4c385a86b877217c1e1bc2eaff58206eabb747 Mon Sep 17 00:00:00 2001 From: Jamie Matthews <jamie.matthews@gmail.com> Date: Tue, 20 Nov 2012 13:25:21 +0000 Subject: [PATCH 77/79] Add example use of SerializerMethodField to docs --- docs/api-guide/fields.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index ebfb5d47d..914d08618 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -328,7 +328,21 @@ This field is always read-only. ## SerializerMethodField -This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. +This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: + + from rest_framework import serializers + from django.contrib.auth.models import User + from django.utils.timezone import now + + class UserSerializer(serializers.ModelSerializer): + + days_since_joined = serializers.SerializerMethodField('get_days_since_joined') + + class Meta: + model = User + + def get_days_since_joined(self, obj): + return (now() - obj.date_joined).days [cite]: http://www.python.org/dev/peps/pep-0020/ [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS From 3227a357cec2475b8295a67e9fd66f644ea5b0cd Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Tue, 20 Nov 2012 23:19:11 +0000 Subject: [PATCH 78/79] Added @irrelative for the mighty fine work. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bd9e4f487..955870d25 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -63,6 +63,7 @@ The following people have helped make REST framework great. * Rob Romano - [robromano] * Eugene Mechanism - [mechanism] * Jonas Liljestrand - [jonlil] +* Justin Davis - [irrelative] Many thanks to everyone who's contributed to the project. @@ -161,3 +162,4 @@ To contact the author directly: [robromano]: https://github.com/robromano [mechanism]: https://github.com/mechanism [jonlil]: https://github.com/jonlil +[irrelative]: https://github.com/irrelative From 3268c67343f6fc6364a0127a7bfabeb907a4751d Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Tue, 20 Nov 2012 23:33:56 +0000 Subject: [PATCH 79/79] Update docs/topics/release-notes.md --- 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 ec83387f5..0b8a7a8f6 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,8 @@ ## Master +* Added `SerializerMethodField` +* Serializer performance improvements. * Added `obtain_token_view` to get tokens when using `TokenAuthentication` * Bugfix: Django 1.5 configurable user support for `TokenAuthentication`