From cc55a7b64310cdd4b8b96e8270a48fd994ede90c Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 18:00:41 +0100 Subject: Returning a Location Header on Create when creating a Resource with HyperlinkedIdentityField of any name --- rest_framework/mixins.py | 19 +++++++++++++++++-- rest_framework/tests/hyperlinkedserializers.py | 8 ++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88..f54b5b1f 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 5ab850af..cc5a19c1 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') -- cgit v1.2.3 From 573de11b233a85347456a4d7e50fd7345d13db03 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 18:07:38 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f54b5b1f..eddd8f49 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 0de01204..88f0019f 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): -- cgit v1.2.3 From 851dff1644f6dafd29838a997a788df51c0570a3 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 18:39:07 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/throttling.py b/rest_framework/tests/throttling.py index 0b94c25b..4b98b941 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): """ -- cgit v1.2.3 From b341dc70af828d066eb3891e8eafb6337cdd2d04 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 19:15:42 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index eddd8f49..832365e5 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 cc5a19c1..5fc935ae 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') -- cgit v1.2.3 From 3a30a9b1cbb4444adf3cbb1d3d80c637b5f4f2ca Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Tue, 13 Nov 2012 20:30:18 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 832365e5..f6119aa1 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): -- cgit v1.2.3 From 5443dd5f3c5f75cd1524eb26c6d5b53df3594f9b Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 13 Nov 2012 23:26:17 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 4c206426..9cd84c0d 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 ebd06e45..d47c39cd 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 c3625a88..991f4c50 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 46d4765e..a46432a9 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) -- cgit v1.2.3 From 8cdbc0a33a69f0a170e92be47189f6006c147137 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 00:09:39 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9cd84c0d..162d2271 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 22fd6e74..dab97346 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 = {} -- cgit v1.2.3 From 8b999c6bb500a045c6c32412009cbd3b1cd5a56b Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Wed, 14 Nov 2012 11:46:16 +0100 Subject: polishing code and adding myself to auhtors file --- rest_framework/mixins.py | 4 ++-- rest_framework/tests/hyperlinkedserializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f6119aa1..089425d5 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 5fc935ae..d7effce7 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') -- cgit v1.2.3 From d9c62c20a7a025d8e94fb881641731b340088f98 Mon Sep 17 00:00:00 2001 From: Ludwig Kraatz Date: Wed, 14 Nov 2012 13:24:20 +0100 Subject: once more polished --- rest_framework/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 089425d5..cd104a7c 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: -- cgit v1.2.3 From 023b065ddc08735c487adff76cc62a864efe1697 Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Wed, 14 Nov 2012 16:02:50 +0100 Subject: added support for passing page_size per request --- rest_framework/mixins.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c3625a88..f725fc9e 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): """ -- cgit v1.2.3 From 44ff2e0add240915d3217ba418178e58e2920419 Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Wed, 14 Nov 2012 19:36:29 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5055bfd3..e38e7c33 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 a231f191..1b710a03 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 4c206426..6ef53975 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 22fd6e74..870464f0 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 0de01204..0bd6c65d 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 46d4765e..0f943ac1 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 906a7cf6..4f10481d 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 316ccd19..0ad926fa 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 1a81101f..bcdc23e7 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 1afbd697..10bdd5a5 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) -- cgit v1.2.3 From 647abcdb16105c51d3414d2da840aeb93b290ef9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 14 Nov 2012 19:34:19 +0000 Subject: Bring keywrod args in line with Django's implementation --- rest_framework/generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ebd06e45..ddb604e0 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 -- cgit v1.2.3 From c35b9eb065b2a9cacaee1dc0849f01f3483e6130 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 21:13:23 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 162d2271..dce31c5a 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 a46432a9..4a13a091 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) -- cgit v1.2.3 From e112a806d863c0d6662dc3a65909ac191c02f03e Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 21:40:52 +0100 Subject: .to_native() now returns the file-name. --- rest_framework/fields.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index dce31c5a..fd57aa2c 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): -- cgit v1.2.3 From 4a2526bd1e067104a1553a3e158016fe9ad285bb Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Sat, 10 Nov 2012 16:09:14 -0800 Subject: 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 (limited to 'rest_framework') diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py new file mode 100644 index 00000000..8e0128c1 --- /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 00000000..2a3e8115 --- /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'), +) -- cgit v1.2.3 From bd92db3c672137fa68185dbc0f453f7cea7caff3 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Sat, 10 Nov 2012 16:17:50 -0800 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 2a3e8115..8bea46c0 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 e69de29b..a52f0a77 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 8ab4c4e4..d1bc23d9 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) -- cgit v1.2.3 From ce3ccb91dc2a7aaf8ff41ac24045c558d641839e Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Mon, 12 Nov 2012 15:16:53 -0800 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 8e0128c1..a5ed6e6d 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 8bea46c0..87872136 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 a52f0a77..e027dff1 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 d1bc23d9..cb16ef1e 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) -- cgit v1.2.3 From 321ba156ca45da8a4b3328c4aec6a9235f32e5f8 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Tue, 13 Nov 2012 16:49:13 -0800 Subject: Renamed AuthTokenView to ObtainAuthToken, added obtain_auth_token var, updated tests & docs. Left authtoken.urls in place as example. --- rest_framework/authtoken/urls.py | 14 +++++++------- rest_framework/authtoken/views.py | 4 +++- rest_framework/tests/authentication.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py index 87872136..a3419da6 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 e027dff1..3ac674e2 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 cb16ef1e..96ca9f52 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'), ) -- cgit v1.2.3 From 535b65a34862e1dcf6370046a37e9cd0e980f491 Mon Sep 17 00:00:00 2001 From: Rob Romano Date: Tue, 13 Nov 2012 17:03:10 -0800 Subject: 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 (limited to 'rest_framework') diff --git a/rest_framework/authtoken/urls.py b/rest_framework/authtoken/urls.py deleted file mode 100644 index a3419da6..00000000 --- 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'), -) -- cgit v1.2.3 From 69a01d71256b9923aac1b8d1b91063068ecfebf7 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 14 Nov 2012 23:04:46 +0100 Subject: Added a test for the FileField. --- rest_framework/tests/files.py | 55 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 25 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/files.py b/rest_framework/tests/files.py index 61d7f7b1..5dd57b7c 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) -- cgit v1.2.3 From 38e94bb8b4e04249b18b9b57ef2ddcb7cfc4efa4 Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Thu, 15 Nov 2012 11:15:05 +0100 Subject: added global and per resource on/off switch + updated docs --- rest_framework/mixins.py | 18 +++++++++++------- rest_framework/settings.py | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index f725fc9e..d64e7e56 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 906a7cf6..1daa9dfd 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 } -- cgit v1.2.3 From 3ae203a0184d27318a8a828ce322b151ade0340f Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Thu, 15 Nov 2012 12:06:43 +0100 Subject: updated script to just use page_size_kwarg --- rest_framework/mixins.py | 11 +++++------ rest_framework/settings.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index d64e7e56..d85e0bfb 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 1daa9dfd..c7b0643f 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' } -- cgit v1.2.3 From a701a21587a69ed959533cbcfdaa9c63337c3ccc Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Thu, 15 Nov 2012 14:35:34 +0100 Subject: added page_size_kwarg tests --- rest_framework/tests/pagination.py | 143 ++++++++++++++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 713a7255..8aae2147 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) -- cgit v1.2.3 From 4edc801d5912b2c31855647b432e461e35322511 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 16 Nov 2012 21:42:04 +0100 Subject: Reproduces #421 --- rest_framework/tests/serializer.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 059593a9..a51df146 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): -- cgit v1.2.3 From aa013a428948802dff9c8ca00df3b7af6faf139b Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 16 Nov 2012 22:18:57 +0100 Subject: Fixes #421 --- rest_framework/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0f943ac1..e5c057fb 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, -- cgit v1.2.3 From f385b72d80b7e9767a6f345496fd108ccc66a4bc Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 16 Nov 2012 22:20:26 +0100 Subject: Whoops … Drop pdb --- rest_framework/serializers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e5c057fb..8f4b7ae2 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 = { -- cgit v1.2.3 From 8d3581f4bd9b0abbf88a7713a1cb8b67f820602a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Nov 2012 21:27:34 +0000 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ddb604e0..9ad03f71 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 cd104a7c..53c4d984 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) -- cgit v1.2.3 From 1a436dd6d9f56b62de61c55c89084d60c09966ba Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 16 Nov 2012 22:43:16 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6ef53975..641a1417 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 8f4b7ae2..dbd9fe27 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 a51df146..fb1be7eb 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): -- cgit v1.2.3 From 31f01bd6315f46bf28bb4c9c25a5298785fc4fc6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Nov 2012 22:45:57 +0000 Subject: Polishing to page size query parameters & more docs --- rest_framework/generics.py | 32 ++++++++++---- rest_framework/mixins.py | 13 ------ rest_framework/settings.py | 9 +++- rest_framework/tests/pagination.py | 85 ++++++-------------------------------- 4 files changed, 43 insertions(+), 96 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 9ad03f71..dcf4dfd9 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 0da4c2cc..53c4d984 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 8883b963..ee24a4ad 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 8aae2147..3062007d 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) -- cgit v1.2.3 From 016ef5019ff43808540f948d674e8dd33247cb99 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Nov 2012 22:58:17 +0000 Subject: Version 2.1.3 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fd176603..88108a8d 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 -- cgit v1.2.3 From acbe991209ed9112af80db99d832704641276844 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 16 Nov 2012 23:22:15 +0000 Subject: Tidying --- rest_framework/generics.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index be225d0a..dd8dfcf8 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): """ -- cgit v1.2.3 From f0d4232c1de2c3154535312d1d48c59854ffa162 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 17:46:16 +0100 Subject: Django 1.5 support, and awareness for AUTH_USER_MODEL --- rest_framework/authtoken/models.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 5b3071aa..610de8d8 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): -- cgit v1.2.3 From 3c1b5c34356eef4ff1a2ecdec26c761c7a27eb27 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 17:53:08 +0100 Subject: indent error --- rest_framework/authtoken/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 610de8d8..08b70f52 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 -- cgit v1.2.3 From bbb5a8a1d90573665c18b70add60d12e8a36882f Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 18:01:46 +0100 Subject: fixed import error --- rest_framework/authtoken/models.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 08b70f52..fa1f4ea6 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): -- cgit v1.2.3 From cd482c0ad22bbb810378c61e02a790e5e3796aa7 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 18:04:37 +0100 Subject: Added support for Django 1.5 for TokenAuth --- rest_framework/authtoken/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index fa1f4ea6..3b8bffeb 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 -- cgit v1.2.3 From 8eb4bb8090a84282c3537641e8ecb5c38a33fc41 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 20:35:15 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 3b8bffeb..4da2aa62 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 e38e7c33..d5ad2a7a 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): -- cgit v1.2.3 From 9f378d0dd44789cb98f3409604d9130d7a0032a8 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sat, 17 Nov 2012 23:51:05 +0100 Subject: fixed bug --- rest_framework/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d5ad2a7a..2f59be95 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.') -- cgit v1.2.3 From d67ee708e5d9f28f26377df391f5e72708e073d2 Mon Sep 17 00:00:00 2001 From: Jacob Magnusson Date: Sun, 18 Nov 2012 18:14:21 +0100 Subject: 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(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c68c39b5..01cf5ae3 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 cbdc765c..59d81150 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 fb1be7eb..814c2499 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): -- cgit v1.2.3 From 91c0249c9d622670252030cb36ea872c08d91471 Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Sun, 18 Nov 2012 21:12:06 +0100 Subject: 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(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 9d750381..2c0c40b1 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 2f59be95..09b76368 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 -- cgit v1.2.3 From b03804fe05225d22c14471a18b96197fdf31dce9 Mon Sep 17 00:00:00 2001 From: glic3rinu Date: Mon, 19 Nov 2012 00:14:03 +0100 Subject: Fixed identation on filter_fields --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/filters.py b/rest_framework/filters.py index ccae4825..bcc87660 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 -- cgit v1.2.3 From 0bcc840927ab08d0e7d64844f3242036a142113d Mon Sep 17 00:00:00 2001 From: Jonas Liljestrand Date: Mon, 19 Nov 2012 11:37:37 +0100 Subject: Complete fix for migration --- rest_framework/authtoken/migrations/0001_initial.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py index 2c0c40b1..f4e052e4 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'"}, -- cgit v1.2.3 From 728e505180d130192c54eec47bd191ab459ebf83 Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Mon, 19 Nov 2012 17:35:28 +0100 Subject: updated to buildin status codes --- rest_framework/renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 870464f0..a2b7704d 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 -- cgit v1.2.3 From de5b071d677074ab3b6b33a843c4b05ba2052a6b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Mon, 19 Nov 2012 17:22:17 +0000 Subject: Add SerializerMethodField --- rest_framework/fields.py | 14 ++++++++++++++ rest_framework/tests/serializer.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c68c39b5..d1e9c45d 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 fb1be7eb..cc6e9d5c 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): -- cgit v1.2.3 From 2cf0fda2ae5cf596946df77675ce10d68587a8bd Mon Sep 17 00:00:00 2001 From: jedavis83@gmail.com Date: Mon, 19 Nov 2012 22:09:40 -0800 Subject: Cache default fields per serializer instance for improved performance --- rest_framework/serializers.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 397866a7..46ffc049 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: -- cgit v1.2.3 From 68c397371c647e88270b8c9e9a6f5f610bbd3a2b Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Tue, 20 Nov 2012 09:41:36 +0000 Subject: 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(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 397866a7..2e7e2cf5 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 59d81150..3704cda7 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 814c2499..9ca4f002 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): -- cgit v1.2.3 From 3cc5349b2f98d9c70788a2aadefc150290316479 Mon Sep 17 00:00:00 2001 From: Jamie Matthews Date: Tue, 20 Nov 2012 09:49:54 +0000 Subject: Clean up and clarify tests for related serializers --- rest_framework/tests/serializer.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 9ca4f002..d522ef97 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) -- cgit v1.2.3