From e029e44477111ed16da8954a53dcf08c08cfb403 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Sat, 10 Nov 2012 16:09:14 -0800 Subject: [PATCH 001/100] 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 002/100] 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 003/100] 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 004/100] 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 005/100] 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 006/100] 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 007/100] 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 008/100] 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 009/100] 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 010/100] 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 011/100] 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 012/100] 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 013/100] 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 014/100] 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 015/100] 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 016/100] 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 017/100] 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 018/100] 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 019/100] 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 020/100] 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 021/100] 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 022/100] 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 023/100] .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 024/100] 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 025/100] 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 026/100] 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 027/100] 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 028/100] 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 029/100] 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 030/100] 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 031/100] 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 032/100] 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 033/100] 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 034/100] 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 035/100] 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 036/100] ./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 037/100] 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 038/100] 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 039/100] 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 040/100] 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 041/100] 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 042/100] =?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 043/100] 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 044/100] 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 045/100] 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 046/100] 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 047/100] 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 048/100] 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 049/100] 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 050/100] 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 051/100] 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 052/100] 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 053/100] 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 054/100] 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 055/100] 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 056/100] 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 057/100] 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 058/100] 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 059/100] 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 060/100] 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 061/100] 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 062/100] 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 063/100] 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 064/100] 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 065/100] 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 066/100] 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 067/100] 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 068/100] 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 069/100] 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 070/100] 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 071/100] 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 072/100] 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 073/100] 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 074/100] 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 075/100] 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 076/100] 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 077/100] 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 86484668f689864aa54e127a8107bdee55240cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Tue, 20 Nov 2012 15:38:50 +0100 Subject: [PATCH 078/100] added RegexField --- docs/api-guide/fields.md | 10 ++++++++++ docs/topics/release-notes.md | 1 + rest_framework/fields.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0485b158f..cb30a52e8 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -141,6 +141,16 @@ A text representation, validates the text to be a valid e-mail address. Corresponds to `django.db.models.fields.EmailField` +## RegexField + +A text representation, that validates the given value matches against a certain regular expression. + +Uses Django's `django.core.validators.RegexValidator` for validation. + +Corresponds to `django.forms.fields.RegexField` + +**Signature:** `RegexField(regex, max_length=None, min_length=None)` + ## DateField A date representation. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 35e8a8b35..e4e676358 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. +* Added `RegexField`. ## 2.1.2 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c2064261..071746de3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,6 +1,7 @@ import copy import datetime import inspect +import re import warnings from django.core import validators @@ -768,6 +769,34 @@ class EmailField(CharField): return result +class RegexField(CharField): + type_name = 'RegexField' + + def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): + super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) + self.regex = regex + + def _get_regex(self): + return self._regex + + def _set_regex(self, regex): + if isinstance(regex, basestring): + regex = re.compile(regex) + self._regex = regex + if hasattr(self, '_regex_validator') and self._regex_validator in self.validators: + self.validators.remove(self._regex_validator) + self._regex_validator = validators.RegexValidator(regex=regex) + self.validators.append(self._regex_validator) + + regex = property(_get_regex, _set_regex) + + def __deepcopy__(self, memo): + result = copy.copy(self) + memo[id(self)] = result + result.validators = self.validators[:] + return result + + class DateField(WritableField): type_name = 'DateField' From 7bf03bbd66008b58d023de312378be17c0a7931d Mon Sep 17 00:00:00 2001 From: Dustin Bachrach <ahdustin@gmail.com> Date: Tue, 20 Nov 2012 08:27:52 -0800 Subject: [PATCH 079/100] Add widgets for DateField and DateTimeField. --- rest_framework/fields.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 01cf5ae3d..1be575a25 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -804,6 +804,7 @@ class EmailField(CharField): class DateField(WritableField): type_name = 'DateField' + widget = widgets.DateInput default_error_messages = { 'invalid': _(u"'%s' value has an invalid date format. It must be " @@ -841,6 +842,7 @@ class DateField(WritableField): class DateTimeField(WritableField): type_name = 'DateTimeField' + widget = widgets.DateTimeInput default_error_messages = { 'invalid': _(u"'%s' value has an invalid format. It must be in " From c3644234cda5c457d72baf1fbf145f12f49a1fa4 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley <maspwr@gmail.com> Date: Tue, 20 Nov 2012 11:01:21 -0800 Subject: [PATCH 080/100] Add support for partial serializer updates --- rest_framework/fields.py | 4 ++-- rest_framework/serializers.py | 3 ++- rest_framework/tests/serializer.py | 12 ++++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 01cf5ae3d..d35e918cd 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -148,7 +148,7 @@ class WritableField(Field): self.widget = widget def validate(self, value): - if value in validators.EMPTY_VALUES and self.required: + if value in validators.EMPTY_VALUES and self.required and not self.root.partial: raise ValidationError(self.error_messages['required']) def run_validators(self, value): @@ -186,7 +186,7 @@ class WritableField(Field): if self.default is not None: native = self.default else: - if self.required: + if self.required and not self.root.partial: raise ValidationError(self.error_messages['required']) return diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2e7e2cf5d..229c1b2cd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -91,12 +91,13 @@ class BaseSerializer(Field): _options_class = SerializerOptions _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations. - def __init__(self, instance=None, data=None, files=None, context=None, **kwargs): + def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.fields = copy.deepcopy(self.base_fields) self.parent = None self.root = None + self.partial = partial self.context = context or {} diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index d522ef972..882f769c0 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -108,6 +108,18 @@ class BasicTests(TestCase): self.assertTrue(serializer.object is expected) self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!') + def test_partial_update(self): + msg = 'Merry New Year!' + partial_data = {'content': msg} + serializer = CommentSerializer(self.comment, data=partial_data) + self.assertEquals(serializer.is_valid(), False) + serializer = CommentSerializer(self.comment, data=partial_data, partial=True) + expected = self.comment + self.assertEqual(serializer.is_valid(), True) + self.assertEquals(serializer.object, expected) + self.assertTrue(serializer.object is expected) + self.assertEquals(serializer.data['content'], msg) + def test_model_fields_as_expected(self): """ Make sure that the fields returned are the same as defined 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 081/100] 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 082/100] 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` From 3b43d41e918b70e5ce83f7da2caabcae2e1bcd72 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley <maspwr@gmail.com> Date: Tue, 20 Nov 2012 15:57:54 -0800 Subject: [PATCH 083/100] Documentation changes for partial serializer updates --- docs/api-guide/serializers.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a95891449..624c41595 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -77,6 +77,10 @@ When deserializing data, we can either create a new instance, or update an exist serializer = CommentSerializer(data=data) # Create new instance serializer = CommentSerializer(comment, data=data) # Update `instance` +By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates. + + serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data + ## Validation When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` and `.non_field_errors` properties will contain the resulting error messages. From 8b0561c57e4684ac440d36b39069a6c7f6168a02 Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Tue, 20 Nov 2012 23:09:47 -0800 Subject: [PATCH 084/100] Cache all fields on serializer init, not just default fields. --- rest_framework/serializers.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f7918c4c3..efd564d41 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -103,7 +103,7 @@ class BaseSerializer(Field): self.init_data = data self.init_files = files self.object = instance - self.default_fields = self.get_default_fields() + self.serialize_fields = self.get_fields() self._data = None self._files = None @@ -134,7 +134,7 @@ class BaseSerializer(Field): field.initialize(parent=self, field_name=key) # Add in the default fields - for key, val in self.default_fields.items(): + for key, val in self.get_default_fields().items(): if key not in ret: ret[key] = val @@ -181,8 +181,7 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - fields = self.get_fields() - for field_name, field in fields.items(): + for field_name, field in self.serialize_fields.items(): key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) ret[key] = value @@ -194,9 +193,8 @@ 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() reverted_data = {} - for field_name, field in fields.items(): + for field_name, field in self.serialize_fields.items(): try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: @@ -208,10 +206,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() - - for field_name, field in fields.items(): + for field_name, field in self.serialize_fields.items(): try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: From e03bb9c2fe53591c40707569d091b8793ea1000e Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Tue, 20 Nov 2012 23:17:30 -0800 Subject: [PATCH 085/100] Change pagination to update Serializer.serialize_fields --- rest_framework/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index d241ade7c..fdffec351 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -62,7 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field object_serializer = self.opts.object_serializer_class - self.fields[results_field] = object_serializer(source='object_list') + self.serialize_fields[results_field] = object_serializer(source='object_list') def to_native(self, obj): """ From ed713d0354b67bdc64de9346b9a72e1adfced76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 21 Nov 2012 11:07:08 +0100 Subject: [PATCH 086/100] added tests --- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index cbdc765c8..f6e5333b1 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -154,3 +154,7 @@ class BlankFieldModel(RESTFrameworkModel): # Model for issue #380 class OptionalRelationModel(RESTFrameworkModel): other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) + +# Model for RegexField +class Book(RESTFrameworkModel): + isbn = models.CharField(max_length=13) \ No newline at end of file diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 059593a90..ad100e539 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -2,7 +2,7 @@ import datetime from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, - BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel, + BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel) @@ -40,6 +40,13 @@ class CommentSerializer(serializers.Serializer): return instance +class BookSerializer(serializers.ModelSerializer): + isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'}) + + class Meta: + model = Book + + class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem @@ -240,6 +247,25 @@ class ValidationTests(TestCase): self.assertEquals(serializer.errors, {}) +class RegexValidationTest(TestCase): + def test_create_failed(self): + serializer = BookSerializer(data={'isbn': '1234567890'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + serializer = BookSerializer(data={'isbn': '12345678901234'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + serializer = BookSerializer(data={'isbn': 'abcdefghijklm'}) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'isbn': [u'isbn has to be exact 13 numbers']}) + + def test_create_success(self): + serializer = BookSerializer(data={'isbn': '1234567890123'}) + self.assertTrue(serializer.is_valid()) + + class MetadataTests(TestCase): def test_empty(self): serializer = CommentSerializer() From 03100168ff96dd4a09ee7c8a5a63b294abe99dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 21 Nov 2012 11:57:00 +0100 Subject: [PATCH 087/100] added missing line --- rest_framework/tests/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 70523fc0d..c35861c6c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -166,6 +166,7 @@ class BlankFieldModel(RESTFrameworkModel): class OptionalRelationModel(RESTFrameworkModel): other = models.ForeignKey('OptionalRelationModel', blank=True, null=True) + # Model for RegexField class Book(RESTFrameworkModel): isbn = models.CharField(max_length=13) From 9459289d7d388074045b726225cb6e140f3c18c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 21 Nov 2012 13:35:20 +0100 Subject: [PATCH 088/100] updated comparison due to pep8 programming recommendations http://www.python.org/dev/peps/pep-0008/#programming-recommendations --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a95891449..048c12006 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -34,7 +34,7 @@ Declaring a serializer looks very similar to declaring a form: created = serializers.DateTimeField() def restore_object(self, attrs, instance=None): - if instance: + if instance is not None: instance.title = attrs['title'] instance.content = attrs['content'] instance.created = attrs['created'] From 834f31ae4d77378f8a56b8647564b50c56bcabb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 21 Nov 2012 14:58:04 +0100 Subject: [PATCH 089/100] added RegexField to field_mapping in BrowsableAPIRenderer --- rest_framework/renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index db1bce39d..27340745a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -312,6 +312,7 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.DateTimeField: forms.DateTimeField, serializers.DateField: forms.DateField, serializers.EmailField: forms.EmailField, + serializers.RegexField: forms.RegexField, serializers.CharField: forms.CharField, serializers.ChoiceField: forms.ChoiceField, serializers.BooleanField: forms.BooleanField, From 774d687a311813a45ac9b2d3e1570c8bbca092fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= <stephan@minddust.com> Date: Wed, 21 Nov 2012 14:58:33 +0100 Subject: [PATCH 090/100] updated comparison due to pep8 programming recommendations http://www.python.org/dev/peps/pep-0008/#programming-recommendations --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f7918c4c3..9f4964fae 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -460,7 +460,7 @@ class ModelSerializer(Serializer): """ self.m2m_data = {} - if instance: + if instance is not None: for key, val in attrs.items(): setattr(instance, key, val) return instance From b0bad35ef0972ec26ff808d81b1f43f16683898d Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Wed, 21 Nov 2012 17:32:20 +0000 Subject: [PATCH 091/100] Tweak to work with serializer performance improvement --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 27340745a..550963cb2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -327,7 +327,7 @@ class BrowsableAPIRenderer(BaseRenderer): } fields = {} - for k, v in serializer.get_fields(True).items(): + for k, v in serializer.get_fields().items(): if getattr(v, 'read_only', True): continue From 1adfc41dc7c7e50edbf72f87ebf62bae33eb212c Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley <maspwr@gmail.com> Date: Wed, 21 Nov 2012 09:36:37 -0800 Subject: [PATCH 092/100] partial argument should override required --- rest_framework/fields.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d35e918cd..b61dcb42a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -53,6 +53,8 @@ class Field(object): self.parent = parent self.root = parent.root or parent self.context = self.root.context + if self.root.partial: + self.required = False def field_from_native(self, data, files, field_name, into): """ @@ -148,7 +150,7 @@ class WritableField(Field): self.widget = widget def validate(self, value): - if value in validators.EMPTY_VALUES and self.required and not self.root.partial: + if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) def run_validators(self, value): @@ -186,7 +188,7 @@ class WritableField(Field): if self.default is not None: native = self.default else: - if self.required and not self.root.partial: + if self.required: raise ValidationError(self.error_messages['required']) return From d031ccce6e693df088faa1b85dc50c0a5e636acf Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Thu, 22 Nov 2012 10:07:42 +0000 Subject: [PATCH 093/100] Updated 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 c641a1b39..99cc9b6d8 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -16,6 +16,7 @@ **Date**: 16th Nov 2012 +* Support for partial updates with serializers. * Added `FileField` and `ImageField`. For use with `MultiPartParser`. * Added `URLField` and `SlugField`. * Support for `read_only_fields` on `ModelSerializer` classes. From df545f7a2560928fdfd8677648aa402e4f2e642b Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Thu, 22 Nov 2012 10:08:14 +0000 Subject: [PATCH 094/100] Updated 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 99cc9b6d8..9235dadab 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,7 @@ ## Master +* Support for partial updates with serializers. * Added `RegexField`. * Added `SerializerMethodField`. * Serializer performance improvements. From 4eaac26427b8574026705cdffbdf9601d620e45b Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Thu, 22 Nov 2012 10:12:22 +0000 Subject: [PATCH 095/100] Added @dbachrach. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 955870d25..01aff6e06 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -64,6 +64,7 @@ The following people have helped make REST framework great. * Eugene Mechanism - [mechanism] * Jonas Liljestrand - [jonlil] * Justin Davis - [irrelative] +* Dustin Bachrach - [dbachrach] Many thanks to everyone who's contributed to the project. @@ -163,3 +164,4 @@ To contact the author directly: [mechanism]: https://github.com/mechanism [jonlil]: https://github.com/jonlil [irrelative]: https://github.com/irrelative +[dbachrach]: https://github.com/dbachrach \ No newline at end of file From db3dc79288f7129ceaf2d8500699920db073cbc3 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Thu, 22 Nov 2012 10:16:47 +0000 Subject: [PATCH 096/100] Added @maspwr for the partial updates work. Ta! --- docs/topics/credits.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 01aff6e06..5323e9c06 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -65,6 +65,7 @@ The following people have helped make REST framework great. * Jonas Liljestrand - [jonlil] * Justin Davis - [irrelative] * Dustin Bachrach - [dbachrach] +* Mark Shirley - [maspwr] Many thanks to everyone who's contributed to the project. @@ -164,4 +165,5 @@ To contact the author directly: [mechanism]: https://github.com/mechanism [jonlil]: https://github.com/jonlil [irrelative]: https://github.com/irrelative -[dbachrach]: https://github.com/dbachrach \ No newline at end of file +[dbachrach]: https://github.com/dbachrach +[maspwr]: https://github.com/maspwr \ No newline at end of file From ac84c2ed2e3571b918f6c995a8f753e86c8126f1 Mon Sep 17 00:00:00 2001 From: Tom Christie <tom@tomchristie.com> Date: Thu, 22 Nov 2012 17:49:53 +0000 Subject: [PATCH 097/100] Version 2.1.4 --- README.md | 11 +++++++++++ docs/topics/release-notes.md | 5 +++-- rest_framework/__init__.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a12d5358..2743dd3e9 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,17 @@ To run the tests. # Changelog +## 2.1.4 + +**Date**: 22nd Nov 2012 + +* Support for partial updates with serializers. +* Added `RegexField`. +* Added `SerializerMethodField`. +* Serializer performance improvements. +* Added `obtain_token_view` to get tokens when using `TokenAuthentication`. +* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`. + ## 2.1.3 **Date**: 16th Nov 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 9235dadab..fe5466be5 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.4 + +**Date**: 22nd Nov 2012 * Support for partial updates with serializers. * Added `RegexField`. @@ -17,7 +19,6 @@ **Date**: 16th Nov 2012 -* Support for partial updates with serializers. * Added `FileField` and `ImageField`. For use with `MultiPartParser`. * Added `URLField` and `SlugField`. * Support for `read_only_fields` on `ModelSerializer` classes. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 88108a8d2..a2233f3df 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.3' +__version__ = '2.1.4' VERSION = __version__ # synonym From e9944f82d1efd7c6bf89ca02fb9e41b9b8973129 Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Thu, 22 Nov 2012 10:50:29 -0800 Subject: [PATCH 098/100] Keep Serializer.fields API consistent while caching values. --- rest_framework/pagination.py | 2 +- rest_framework/serializers.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index fdffec351..d241ade7c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -62,7 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field object_serializer = self.opts.object_serializer_class - self.serialize_fields[results_field] = object_serializer(source='object_list') + self.fields[results_field] = object_serializer(source='object_list') def to_native(self, obj): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index efd564d41..380c67e46 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -60,7 +60,7 @@ def _get_declared_fields(bases, attrs): # If this class is subclassing another Serializer, add that Serializer's # fields. Note that we loop over the bases in *reverse*. This is necessary - # in order to the correct order of fields. + # in order to maintain the correct order of fields. for base in bases[::-1]: if hasattr(base, 'base_fields'): fields = base.base_fields.items() + fields @@ -94,7 +94,6 @@ class BaseSerializer(Field): 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) self.parent = None self.root = None @@ -103,7 +102,7 @@ class BaseSerializer(Field): self.init_data = data self.init_files = files self.object = instance - self.serialize_fields = self.get_fields() + self.fields = self.get_fields() self._data = None self._files = None @@ -128,7 +127,8 @@ class BaseSerializer(Field): ret = SortedDict() # Get the explicitly declared fields - for key, field in self.fields.items(): + base_fields = copy.deepcopy(self.base_fields) + for key, field in base_fields.items(): ret[key] = field # Set up the field field.initialize(parent=self, field_name=key) @@ -181,7 +181,7 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - for field_name, field in self.serialize_fields.items(): + for field_name, field in self.fields.items(): key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) ret[key] = value @@ -194,7 +194,7 @@ class BaseSerializer(Field): Converts a dictionary of data into a dictionary of deserialized fields. """ reverted_data = {} - for field_name, field in self.serialize_fields.items(): + for field_name, field in self.fields.items(): try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: @@ -206,7 +206,7 @@ class BaseSerializer(Field): """ Run `validate_<fieldname>()` and `validate()` methods on the serializer """ - for field_name, field in self.serialize_fields.items(): + for field_name, field in self.fields.items(): try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: From 08e7818530757fe4b5fb75ba9c6b1db2bf54ee5d Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Thu, 22 Nov 2012 11:27:55 -0800 Subject: [PATCH 099/100] More consistent iteration over default_fields, per feedback. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 380c67e46..24fbcb79a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -134,7 +134,8 @@ class BaseSerializer(Field): field.initialize(parent=self, field_name=key) # Add in the default fields - for key, val in self.get_default_fields().items(): + default_fields = self.get_default_fields() + for key, val in self.default_fields.items(): if key not in ret: ret[key] = val From 2e36e0c9106ad8de49ce8c169083ec1d09448458 Mon Sep 17 00:00:00 2001 From: "jedavis83@gmail.com" <justin@chartiojustin.boat.io> Date: Thu, 22 Nov 2012 12:22:30 -0800 Subject: [PATCH 100/100] Remove unneeded and incorrect self reference --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 24fbcb79a..546abc061 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -135,7 +135,7 @@ class BaseSerializer(Field): # Add in the default fields default_fields = self.get_default_fields() - for key, val in self.default_fields.items(): + for key, val in default_fields.items(): if key not in ret: ret[key] = val