From ca5b99486d15e7392754178ab0948de2a60763a3 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 22 Nov 2012 22:36:37 +0100 Subject: [PATCH 001/129] Added _post_clean() behaviour by adding a .perform_model_validation() method. Fixed some tests that were failing due to extra strict validation. --- rest_framework/serializers.py | 57 +++++++++++++++++++++++++++--- rest_framework/tests/models.py | 4 +-- rest_framework/tests/serializer.py | 2 +- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 53dcec163..6d5b4cb53 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -428,10 +428,6 @@ class ModelSerializer(Serializer): kwargs['choices'] = model_field.flatchoices return ChoiceField(**kwargs) - max_length = getattr(model_field, 'max_length', None) - if max_length: - kwargs['max_length'] = max_length - field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, @@ -455,6 +451,59 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) + def from_native(self, data, files): + restored_object = super(ModelSerializer, self).from_native(data, files) + + if restored_object is None: + return + + self.perform_model_validation(restored_object) + return restored_object + + def perform_model_validation(self, restored_object): + + # if hasattr(restored_object, '__iter__'): # Iterables are not model instances + # return restored_object + #self._errors[field_name] = list(err.messages) + +# opts = self._meta + # Update the model instance with self.cleaned_data. +# instance = construct_instance(self, self.instance, opts.fields, opts.exclude) + +# exclude = self._get_validation_exclusions() + + # Foreign Keys being used to represent inline relationships + # are excluded from basic field value validation. This is for two + # reasons: firstly, the value may not be supplied (#12507; the + # case of providing new values to the admin); secondly the + # object being referred to may not yet fully exist (#12749). + # However, these fields *must* be included in uniqueness checks, + # so this can't be part of _get_validation_exclusions(). +# for f_name, field in self.fields.items(): +# if isinstance(field, InlineForeignKeyField): +# exclude.append(f_name) + + # Clean the model instance's fields. + try: + restored_object.clean_fields() # exclude=exclude) + except ValidationError as e: + for field_name, error_messages in e.message_dict.items(): + self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) + + # Call the model instance's clean method. + try: + restored_object.clean() + except ValidationError as e: + for field_name, error_messages in e.message_dict.items(): + self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) + + # Validate uniqueness if needed. + # exclude = self._get_validation_exclusions() +# try: +# restored_object.validate_unique() # exclude=exclude) +# except ValidationError as e: +# model_errors.append(e.message_dict) + def restore_object(self, attrs, instance=None): """ Restore the model instance. diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index c35861c6c..9a59e8411 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -61,7 +61,7 @@ class BasicModel(RESTFrameworkModel): class SlugBasedModel(RESTFrameworkModel): text = models.CharField(max_length=100) - slug = models.SlugField(max_length=32) + slug = models.SlugField(max_length=32, blank=True) class DefaultValueModel(RESTFrameworkModel): @@ -159,7 +159,7 @@ class Person(RESTFrameworkModel): # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True) + title = models.CharField(max_length=100, blank=True, null=True) # Model for issue #380 diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 61a05da18..5751e8940 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -169,7 +169,7 @@ class ValidationTests(TestCase): 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) } - self.actionitem = ActionItem('Some to do item', + self.actionitem = ActionItem(title='Some to do item', ) def test_create(self): From bd8c742df2cc72896fa975196fdf56961e89cd94 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 22 Nov 2012 23:39:16 +0100 Subject: [PATCH 002/129] Cleanup. --- rest_framework/serializers.py | 40 ++--------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6d5b4cb53..bb15faa8a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -461,49 +461,13 @@ class ModelSerializer(Serializer): return restored_object def perform_model_validation(self, restored_object): - - # if hasattr(restored_object, '__iter__'): # Iterables are not model instances - # return restored_object - #self._errors[field_name] = list(err.messages) - -# opts = self._meta - # Update the model instance with self.cleaned_data. -# instance = construct_instance(self, self.instance, opts.fields, opts.exclude) - -# exclude = self._get_validation_exclusions() - - # Foreign Keys being used to represent inline relationships - # are excluded from basic field value validation. This is for two - # reasons: firstly, the value may not be supplied (#12507; the - # case of providing new values to the admin); secondly the - # object being referred to may not yet fully exist (#12749). - # However, these fields *must* be included in uniqueness checks, - # so this can't be part of _get_validation_exclusions(). -# for f_name, field in self.fields.items(): -# if isinstance(field, InlineForeignKeyField): -# exclude.append(f_name) - - # Clean the model instance's fields. try: - restored_object.clean_fields() # exclude=exclude) + # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique() + restored_object.full_clean(exclude=list(self.opts.exclude)) except ValidationError as e: for field_name, error_messages in e.message_dict.items(): self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) - # Call the model instance's clean method. - try: - restored_object.clean() - except ValidationError as e: - for field_name, error_messages in e.message_dict.items(): - self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) - - # Validate uniqueness if needed. - # exclude = self._get_validation_exclusions() -# try: -# restored_object.validate_unique() # exclude=exclude) -# except ValidationError as e: -# model_errors.append(e.message_dict) - def restore_object(self, attrs, instance=None): """ Restore the model instance. From 3f47f6cea9d178a57855e3b90208601b7e28a80f Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 22 Nov 2012 23:50:42 +0100 Subject: [PATCH 003/129] Added a validate_unique test. --- rest_framework/tests/serializer.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 5751e8940..0baf0e89d 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,7 +1,7 @@ import datetime from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, +from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel) @@ -48,7 +48,7 @@ class BookSerializer(serializers.ModelSerializer): class ActionItemSerializer(serializers.ModelSerializer): - + class Meta: model = ActionItem @@ -62,6 +62,12 @@ class PersonSerializer(serializers.ModelSerializer): read_only_fields = ('age',) +class AlbumsSerializer(serializers.ModelSerializer): + + class Meta: + model = Album + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -276,6 +282,16 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) + def test_validate_unique(self): + """ + Just check if serializers.ModelSerializer.perform_model_validation() handles unique checks via .full_clean() + """ + serializer = AlbumsSerializer(data={'title': 'a'}) + serializer.is_valid() + serializer.save() + second_serializer = AlbumsSerializer(data={'title': 'a'}) + self.assertFalse(second_serializer.is_valid()) + class RegexValidationTest(TestCase): def test_create_failed(self): From 85a921c7efcea50a5d594082f0e4ddeefd95402f Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Sat, 24 Nov 2012 17:18:32 +0000 Subject: [PATCH 004/129] Added setter to user property --- rest_framework/request.py | 9 +++++++++ rest_framework/tests/request.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/rest_framework/request.py b/rest_framework/request.py index a1827ba48..39c643219 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -169,6 +169,15 @@ class Request(object): self._user, self._auth = self._authenticate() return self._user + @user.setter + def user(self, value): + """ + Sets the user on the current request. This is necessary to maintain + compatilbility with django.contrib.auth where the user proprety is + set in the login and logout functions. + """ + self._user = value + @property def auth(self): """ diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index ff48f3fa3..2850992d2 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -3,6 +3,8 @@ Tests for content parsing, and form-overloaded content parsing. """ from django.conf.urls.defaults import patterns from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login, logout +from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, Client from django.utils import simplejson as json @@ -276,3 +278,29 @@ class TestContentParsingWithAuthentication(TestCase): # response = self.csrf_client.post('/', content) # self.assertEqual(status.OK, response.status_code, "POST data is malformed") + + +class TestUserSetter(TestCase): + + def setUp(self): + # Pass request object through session middleware so session is + # available to login and logout functions + self.request = Request(factory.get('/')) + SessionMiddleware().process_request(self.request) + + User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow') + self.user = authenticate(username='ringo', password='yellow') + + def test_user_can_be_set(self): + self.request.user = self.user + self.assertEqual(self.request.user, self.user) + + def test_user_can_login(self): + login(self.request, self.user) + self.assertEqual(self.request.user, self.user) + + def test_user_can_logout(self): + self.request.user = self.user + self.assertFalse(self.request.user.is_anonymous()) + logout(self.request) + self.assertTrue(self.request.user.is_anonymous()) From e7666014a85d65e204b40e1f54911e654f974932 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Mon, 26 Nov 2012 23:39:49 +0100 Subject: [PATCH 005/129] Added an assertion to the tests that checks the '.errors' value for the unique-test --- rest_framework/tests/serializer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 0baf0e89d..bdf72a91b 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -291,6 +291,7 @@ class ValidationTests(TestCase): serializer.save() second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) + self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']}) class RegexValidationTest(TestCase): From f104f7434052bedf6dd970806ff54b73489b339b Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 27 Nov 2012 23:21:12 +0100 Subject: [PATCH 006/129] Moved model validation from .perform_validation() to .validate() --- rest_framework/serializers.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bb15faa8a..5046c7b1e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -221,10 +221,17 @@ class BaseSerializer(Field): except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) - try: - attrs = self.validate(attrs) - except ValidationError as err: - self._errors['non_field_errors'] = err.messages + # We don't run .validate() because field-validation failed and thus `attrs` may not be complete. + # which in turn can cause inconsistent validation errors. + if not self._errors: + try: + attrs = self.validate(attrs) + except ValidationError as err: + if hasattr(err, 'message_dict'): + for field_name, error_messages in err.message_dict.items(): + self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) + elif hasattr(err, 'messages'): + self._errors['non_field_errors'] = err.messages return attrs @@ -451,22 +458,15 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def from_native(self, data, files): - restored_object = super(ModelSerializer, self).from_native(data, files) - - if restored_object is None: - return - + def validate(self, attrs): + copied_attrs = copy.deepcopy(attrs) + restored_object = self.restore_object(copied_attrs, instance=getattr(self, 'object', None)) self.perform_model_validation(restored_object) - return restored_object + return attrs def perform_model_validation(self, restored_object): - try: - # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique() - restored_object.full_clean(exclude=list(self.opts.exclude)) - except ValidationError as e: - for field_name, error_messages in e.message_dict.items(): - self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages) + # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique() + restored_object.full_clean(exclude=list(self.opts.exclude)) def restore_object(self, attrs, instance=None): """ From 899f96ae9186e68009dba5d54246232d34457354 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Tue, 27 Nov 2012 23:49:27 +0100 Subject: [PATCH 007/129] Added a get_excluded_fieldnames() method. Model validation now excludes fields not listed in Meta fields (if set). --- rest_framework/serializers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5046c7b1e..775a8a1e2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -119,6 +119,17 @@ class BaseSerializer(Field): """ return {} + def get_excluded_fieldnames(self): + """ + Returns the fieldnames that should not be validated. + """ + excluded_fields = list(self.opts.exclude) + for field in self.fields.keys() + self.default_fields.keys(): + if self.opts.fields: + if field not in self.opts.fields + self.opts.exclude: + excluded_fields.append(field) + return excluded_fields + def get_fields(self): """ Returns the complete set of fields for the object as a dict. @@ -466,7 +477,7 @@ class ModelSerializer(Serializer): def perform_model_validation(self, restored_object): # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique() - restored_object.full_clean(exclude=list(self.opts.exclude)) + restored_object.full_clean(exclude=list(self.get_excluded_fieldnames())) def restore_object(self, attrs, instance=None): """ From e311b763e193b41c6a679ddbcf813702691145a0 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Fri, 30 Nov 2012 01:34:46 +0200 Subject: [PATCH 008/129] add traverse_related feature + tests (fixes issue#461) --- rest_framework/serializers.py | 14 +++++++--- rest_framework/tests/models.py | 25 +++++++++--------- rest_framework/tests/serializer.py | 41 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4519ab053..e63f47835 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -272,10 +272,18 @@ class BaseSerializer(Field): Override default so that we can apply ModelSerializer as a nested field to relationships. """ - obj = getattr(obj, self.source or field_name) - if is_simple_callable(obj): - obj = obj() + if self.source: + value = obj + for component in self.source.split('.'): + value = getattr(value, component) + if is_simple_callable(value): + value = value() + obj = value + else: + value = getattr(obj, field_name) + if is_simple_callable(value): + obj = value() # If the object has an "all" method, assume it's a relationship if is_simple_callable(getattr(obj, 'all', None)): diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index c35861c6c..76435df84 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -124,8 +124,21 @@ class ActionItem(RESTFrameworkModel): # Models for reverse relations +class Person(RESTFrameworkModel): + name = models.CharField(max_length=10) + age = models.IntegerField(null=True, blank=True) + + @property + def info(self): + return { + 'name': self.name, + 'age': self.age, + } + + class BlogPost(RESTFrameworkModel): title = models.CharField(max_length=100) + writer = models.ForeignKey(Person, null=True, blank=True) def get_first_comment(self): return self.blogpostcomment_set.all()[0] @@ -145,18 +158,6 @@ class Photo(RESTFrameworkModel): album = models.ForeignKey(Album) -class Person(RESTFrameworkModel): - name = models.CharField(max_length=10) - age = models.IntegerField(null=True, blank=True) - - @property - def info(self): - return { - 'name': self.name, - 'age': self.age, - } - - # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): title = models.CharField(max_length=100, blank=True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 61a05da18..b16f2772f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -560,6 +560,47 @@ class ManyRelatedTests(TestCase): self.assertEqual(serializer.data, expected) +class RelatedTraversalTest(TestCase): + def test_nested_traversal(self): + user = Person.objects.create(name="django") + post = BlogPost.objects.create(title="Test blog post", writer=user) + post.blogpostcomment_set.create(text="I love this blog post") + + from rest_framework.tests.models import BlogPostComment + + class PersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + fields = ("name", "age") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = BlogPostComment + fields = ("text", "post_owner") + + text = serializers.CharField() + post_owner = PersonSerializer(source='blog_post.writer') + + class BlogPostSerializer(serializers.Serializer): + title = serializers.CharField() + comments = BlogPostCommentSerializer(source='blogpostcomment_set') + + serializer = BlogPostSerializer(instance=post) + + expected = { + 'title': 'Test blog post', + 'comments': [{ + 'text': 'I hate this blog post', + 'post_owner': { + "name": "django", + "age": None + } + }] + } + + self.assertEqual(serializer.data, expected) + + class SerializerMethodFieldTests(TestCase): def setUp(self): From 1c1bd3fc5d7e65ae8c16e9946be87956c96a1723 Mon Sep 17 00:00:00 2001 From: Pavel Savchenko Date: Fri, 30 Nov 2012 01:37:21 +0200 Subject: [PATCH 009/129] fix test response --- rest_framework/tests/serializer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index b16f2772f..26a7d6bfd 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -588,11 +588,11 @@ class RelatedTraversalTest(TestCase): serializer = BlogPostSerializer(instance=post) expected = { - 'title': 'Test blog post', + 'title': u'Test blog post', 'comments': [{ - 'text': 'I hate this blog post', + 'text': u'I love this blog post', 'post_owner': { - "name": "django", + "name": u"django", "age": None } }] From 45d28f49e03a394bfcfc6348558ec5bd4bac2b6c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 2 Dec 2012 11:04:34 -0400 Subject: [PATCH 010/129] Added @mhsparks. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e0c589b23..75a2d596c 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -69,6 +69,7 @@ The following people have helped make REST framework great. * Olivier Aubert - [oaubert] * Yuri Prezument - [yprez] * Fabian Buechler - [fabianbuechler] +* Mark Hughes - [mhsparks] Many thanks to everyone who's contributed to the project. @@ -173,3 +174,4 @@ To contact the author directly: [oaubert]: https://github.com/oaubert [yprez]: https://github.com/yprez [fabianbuechler]: https://github.com/fabianbuechler +[mhsparks]: https://github.com/mhsparks From 3e3ede71d2f4826fa1d07523705dd53ab2cba29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 3 Dec 2012 12:47:12 +0100 Subject: [PATCH 011/129] Added @mvdwaeter. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 75a2d596c..f2f09c0e8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -70,6 +70,7 @@ The following people have helped make REST framework great. * Yuri Prezument - [yprez] * Fabian Buechler - [fabianbuechler] * Mark Hughes - [mhsparks] +* Michael van de Waeter - [mvdwaeter] Many thanks to everyone who's contributed to the project. @@ -175,3 +176,4 @@ To contact the author directly: [yprez]: https://github.com/yprez [fabianbuechler]: https://github.com/fabianbuechler [mhsparks]: https://github.com/mhsparks +[mvdwaeter]: https://github.com/mvdwaeter From e044fa089b5ccdcc3557a65c106fad0f44f1b7b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Tue, 4 Dec 2012 09:40:23 +0100 Subject: [PATCH 012/129] fixed #469 - RegexField <--> BrowsableAPI Bug --- docs/topics/release-notes.md | 4 ++++ rest_framework/fields.py | 1 + rest_framework/renderers.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 867b138bf..c2fe3f64d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,6 +4,10 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. +## Master + +* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer` + ## 2.1.6 **Date**: 23rd Nov 2012 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 482a3d485..ea0667f50 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -817,6 +817,7 @@ class EmailField(CharField): class RegexField(CharField): type_name = 'RegexField' + form_field_class = forms.RegexField def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 25a32baae..1220bca10 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -320,6 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'choices', None) is not None: kwargs['choices'] = v.choices + if getattr(v, 'regex', None) is not None: + kwargs['regex'] = v.regex + if getattr(v, 'widget', None): widget = copy.deepcopy(v.widget) kwargs['widget'] = widget From 3867d9deb18d132ec5e0325370c77c2cf9aa0215 Mon Sep 17 00:00:00 2001 From: Michael Richards Date: Tue, 4 Dec 2012 11:07:31 -0800 Subject: [PATCH 013/129] Added support for 'true'/'false' as valid boolean data --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 482a3d485..ff39fac48 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -693,9 +693,9 @@ class BooleanField(WritableField): default = False def from_native(self, value): - if value in ('t', 'True', '1'): + if value in ('true', 't', 'True', '1'): return True - if value in ('f', 'False', '0'): + if value in ('false', 'f', 'False', '0'): return False return bool(value) From fc6dbb45e023a5e5e6c92bd434b93350c4fbb8d3 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 12:20:03 +0100 Subject: [PATCH 014/129] Fixed wording. --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 9a36a2b0d..74084541d 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -137,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a 'PAGINATE_BY': 10 } -Okay, that's us done. +Okay, we're done. --- From 3417c4631d0680aee14f7b06435d00c25ce5b464 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 12:31:38 +0100 Subject: [PATCH 015/129] Fixed typos and fixed wording. Some singular/plural fixes. Fixed some 'serialise->serialize' kind of UK/US differences. The 'z' seems more common in the rest of the docs, so that's what I used. Removed a half-finished-sentence left dangling somewhere. --- docs/tutorial/1-serialization.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index ba64f2aa7..e61fb9469 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -14,7 +14,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o ## Setting up a new environment -Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on. +Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on. :::bash mkdir ~/env @@ -39,7 +39,6 @@ To get started, let's create a new project to work with. cd tutorial Once that's done we can create an app that we'll use to create a simple Web API. -We're going to create a project that python manage.py startapp snippets @@ -64,7 +63,7 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I 'snippets' ) -We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views. +We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. urlpatterns = patterns('', url(r'^', include('snippets.urls')), @@ -105,7 +104,7 @@ Don't forget to sync the database for the first time. ## Creating a Serializer class -The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. +The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. from django.forms import widgets from rest_framework import serializers @@ -146,7 +145,7 @@ We can actually also save ourselves some time by using the `ModelSerializer` cla ## Working with Serializers -Before we go any further we'll familiarise ourselves with using our new Serializer class. Let's drop into the Django shell. +Before we go any further we'll familiarize ourselves with using our new Serializer class. Let's drop into the Django shell. python manage.py shell @@ -166,7 +165,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer.data # {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} -At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`. +At this point we've translated the model instance into python native datatypes. To finalize the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content @@ -292,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file: url(r'^snippets/(?P[0-9]+)/$', 'snippet_detail') ) -It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now. +It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now. ## Testing our first attempt at a Web API @@ -304,7 +303,7 @@ It's worth noting that there's a couple of edge cases we're not dealing with pro We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views. -Our API views don't do anything particularly special at the moment, beyond serve `json` responses, and there's some error handling edge cases we'd still like to clean up, but it's a functioning Web API. +Our API views don't do anything particularly special at the moment, beyond serving `json` responses, and there are some error handling edge cases we'd still like to clean up, but it's a functioning Web API. We'll see how we can start to improve things in [part 2 of the tutorial][tut-2]. From 3868241f6acda96fbd08cc81211b09ffbc2f38b3 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Wed, 5 Dec 2012 15:09:06 +0100 Subject: [PATCH 016/129] Update docs/api-guide/permissions.md @permission_classes takes a tuple or list. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 1a746fb64..fce68f6db 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -53,7 +53,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @permission_classes(IsAuthenticated) + @permission_classes((IsAuthenticated, )) def example_view(request, format=None): content = { 'status': 'request was permitted' From cb4e85721717517c9afd86c5e5e027ba19885b27 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 16:04:52 +0100 Subject: [PATCH 017/129] Textual fixes. Added a sentence introducing the second view. Fix one or two additional sentences. --- docs/tutorial/2-requests-and-responses.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 187effb9d..08cf91cdb 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -66,6 +66,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. +Here is the view for an individual snippet. + @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): """ @@ -92,7 +94,7 @@ Our instance view is an improvement over the previous example. It's a little mo snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) -This should all feel very familiar - there's not a lot different to working with regular Django views. +This should all feel very familiar - it is not a lot different from working with regular Django views. Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us. @@ -128,7 +130,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][ **TODO: Describe using accept headers, content-type headers, and format suffixed URLs** -Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]." +Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]. ### Browsability From ee184b86292d347ba747ee4a438f17e4fc613947 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 16:08:13 +0100 Subject: [PATCH 018/129] Small textual fixes. --- docs/tutorial/3-class-based-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index d87d20464..a3a18060f 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes. def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) -We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`. +We'll take a moment to examine exactly what's happening here. We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`. The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. @@ -142,7 +142,7 @@ Using the mixin classes we've rewritten the views to use slightly less code than model = Snippet serializer_class = SnippetSerializer -Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django. +Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django. Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API. From 3f39828788d856bb7923bfb3acf801e571597e55 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 16:16:46 +0100 Subject: [PATCH 019/129] Small textual fixes. --- docs/tutorial/4-authentication-and-permissions.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index f85250bea..9576a7f0b 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -61,7 +61,7 @@ Now that we've got some users to work with, we'd better add representations of t model = User fields = ('id', 'username', 'snippets') -Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we've needed to add an explicit field for it. +Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it. We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. @@ -92,9 +92,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin ## Updating our serializer -Now that snippets are associated with the user that created them, let's update our SnippetSerializer to reflect that. - -Add the following field to the serializer definition: +Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition: owner = serializers.Field(source='owner.username') @@ -108,7 +106,7 @@ The field we've added is the untyped `Field` class, in contrast to the other typ ## Adding required permissions to views -Now that code snippets are associated with users we want to make sure that only authenticated users are able to create, update and delete code snippets. +Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update and delete code snippets. REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access. From 7a110a3006b47e61c12dd5ec9e62b278d1b17298 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Wed, 5 Dec 2012 16:24:41 +0100 Subject: [PATCH 020/129] Two typo fixes. Plural/singular fix. Typo fixed. --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 98c45b821..b5d378751 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -25,7 +25,7 @@ Notice that we're using REST framework's `reverse` function in order to return f The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints. -Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two style of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint. +Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint. The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance. @@ -151,7 +151,7 @@ We could also customize the pagination style if we needed too, but in this case If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links. -You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the hightlighted code HTML representations. +You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations. We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats. From 705c7ad09db65c6ea6fb69bbd417cb7a45f6e3b9 Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Wed, 5 Dec 2012 17:43:47 -0700 Subject: [PATCH 021/129] added tests and fix for unpickleable metadata in SortedDictWithMetadata --- rest_framework/serializers.py | 12 +++++++- rest_framework/tests/serializer.py | 47 ++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4519ab053..fcc0744a3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -22,7 +22,16 @@ class DictWithMetadata(dict): """ A dict-like object, that can have additional properties attached. """ - pass + + def __getstate__(self): + """ Used by pickle (e.g., caching). + Overriden to remove metadata from the dict, since it shouldn't be pickled + and may in some instances be unpickleable. + """ + # return an instance of the first dict in MRO that isn't a DictWithMetadata + for base in self.__class__.__mro__: + if not isinstance(base, DictWithMetadata) and isinstance(base, dict): + return base(self) class SortedDictWithMetadata(SortedDict, DictWithMetadata): @@ -32,6 +41,7 @@ class SortedDictWithMetadata(SortedDict, DictWithMetadata): pass + def _is_protected_type(obj): """ True if the object is a native datatype that does not need to diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 61a05da18..af182917f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,9 +1,9 @@ -import datetime +import datetime, pickle from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, BlogPostComment) class SubComment(object): @@ -641,3 +641,46 @@ class BlankFieldTests(TestCase): """ serializer = self.not_blank_model_serializer_class(data=self.data) self.assertEquals(serializer.is_valid(), False) + + +#class PersonGroup(object): +# name = "group" +# persons = [Person(name="joe"), Person(name="job")] +# +#class PersonGroupSerializer(serializers.Serializer): +# name = serializers.CharField() +# persons = PersonSerializer() +# +#class BlogPostSerializer(serializers.ModelSerializer): +# class Meta: +# model = BlogPost +# +# +#class BlogPostCommentSerializer(serializers.ModelSerializer): +# class Meta: +# model = BlogPostComment +# fields = ('text', 'blog_post') +# +# blog_post = BlogPostSerializer() +# + +#test for issue #460 +class SerializerPickleTests(TestCase): + """ Test pickleability of the output of Serializers + """ + def test_pickle_simple_model_serializer_data(self): + """ Test simple serializer + """ + pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data) + + + def test_pickle_inner_serializer(self): + """ Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will + have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle. + See DictWithMetadata.__getstate__ + """ + class InnerPersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + fields = ('name', 'age') + pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data) From 7f28a784146b9ba6ab303e79597f85a0f8b1e76e Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Wed, 5 Dec 2012 17:54:21 -0700 Subject: [PATCH 022/129] cleaned up last commit --- rest_framework/tests/serializer.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index af182917f..9cedb54b4 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -3,7 +3,7 @@ from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, BlogPostComment) + ManyToManyModel, Person, ReadOnlyManyToManyModel) class SubComment(object): @@ -643,27 +643,6 @@ class BlankFieldTests(TestCase): self.assertEquals(serializer.is_valid(), False) -#class PersonGroup(object): -# name = "group" -# persons = [Person(name="joe"), Person(name="job")] -# -#class PersonGroupSerializer(serializers.Serializer): -# name = serializers.CharField() -# persons = PersonSerializer() -# -#class BlogPostSerializer(serializers.ModelSerializer): -# class Meta: -# model = BlogPost -# -# -#class BlogPostCommentSerializer(serializers.ModelSerializer): -# class Meta: -# model = BlogPostComment -# fields = ('text', 'blog_post') -# -# blog_post = BlogPostSerializer() -# - #test for issue #460 class SerializerPickleTests(TestCase): """ Test pickleability of the output of Serializers From cb7d9ea5c9843ffa99db4400670a11c3651520cc Mon Sep 17 00:00:00 2001 From: Ben Roberts Date: Thu, 6 Dec 2012 12:45:50 -0700 Subject: [PATCH 023/129] cleaned up white space & docstring styling --- rest_framework/serializers.py | 9 ++++----- rest_framework/tests/serializer.py | 14 ++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fcc0744a3..51e0b6647 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -22,11 +22,11 @@ class DictWithMetadata(dict): """ A dict-like object, that can have additional properties attached. """ - def __getstate__(self): - """ Used by pickle (e.g., caching). - Overriden to remove metadata from the dict, since it shouldn't be pickled - and may in some instances be unpickleable. + """ + Used by pickle (e.g., caching). + Overriden to remove metadata from the dict, since it shouldn't be pickled + and may in some instances be unpickleable. """ # return an instance of the first dict in MRO that isn't a DictWithMetadata for base in self.__class__.__mro__: @@ -41,7 +41,6 @@ class SortedDictWithMetadata(SortedDict, DictWithMetadata): pass - def _is_protected_type(obj): """ True if the object is a native datatype that does not need to diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 9cedb54b4..31dd36994 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -645,18 +645,20 @@ class BlankFieldTests(TestCase): #test for issue #460 class SerializerPickleTests(TestCase): - """ Test pickleability of the output of Serializers + """ + Test pickleability of the output of Serializers """ def test_pickle_simple_model_serializer_data(self): - """ Test simple serializer + """ + Test simple serializer """ pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data) - def test_pickle_inner_serializer(self): - """ Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will - have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle. - See DictWithMetadata.__getstate__ + """ + Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will + have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle. + See DictWithMetadata.__getstate__ """ class InnerPersonSerializer(serializers.ModelSerializer): class Meta: From 2938bc13b12ec73084c21e629bdde4a20a1de0cb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Dec 2012 16:30:22 -0400 Subject: [PATCH 024/129] Added @reinout for the copy fixes. Thanks! --- docs/topics/credits.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index f2f09c0e8..a2e0a23fc 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -71,6 +71,7 @@ The following people have helped make REST framework great. * Fabian Buechler - [fabianbuechler] * Mark Hughes - [mhsparks] * Michael van de Waeter - [mvdwaeter] +* Reinout van Rees - [reinout] Many thanks to everyone who's contributed to the project. @@ -177,3 +178,5 @@ To contact the author directly: [fabianbuechler]: https://github.com/fabianbuechler [mhsparks]: https://github.com/mhsparks [mvdwaeter]: https://github.com/mvdwaeter +[reinout]: https://github.com/reinout + From 6a5f4f2a90ab19a8586a9d762c9b2618e8db5c30 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Dec 2012 22:38:20 +0000 Subject: [PATCH 025/129] Added @justanotherbody. Thanks! --- docs/topics/credits.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index a2e0a23fc..9e59d678f 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -72,6 +72,7 @@ The following people have helped make REST framework great. * Mark Hughes - [mhsparks] * Michael van de Waeter - [mvdwaeter] * Reinout van Rees - [reinout] +* Michael Richards - [justanotherbody] Many thanks to everyone who's contributed to the project. @@ -179,4 +180,4 @@ To contact the author directly: [mhsparks]: https://github.com/mhsparks [mvdwaeter]: https://github.com/mvdwaeter [reinout]: https://github.com/reinout - +[justanotherbody]: https://github.com/justanotherbody From 26cfa023263576258e53fe23bc92e437398ff15f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Dec 2012 22:56:23 +0000 Subject: [PATCH 026/129] Added @roberts81. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9e59d678f..dfa1ee0f8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -73,6 +73,7 @@ The following people have helped make REST framework great. * Michael van de Waeter - [mvdwaeter] * Reinout van Rees - [reinout] * Michael Richards - [justanotherbody] +* Ben Roberts - [roberts81] Many thanks to everyone who's contributed to the project. @@ -181,3 +182,4 @@ To contact the author directly: [mvdwaeter]: https://github.com/mvdwaeter [reinout]: https://github.com/reinout [justanotherbody]: https://github.com/justanotherbody +[roberts81]: https://github.com/roberts81 From 919aff329ee1bd214831095e4d96af71795ed572 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Fri, 7 Dec 2012 00:08:27 +0100 Subject: [PATCH 027/129] Fix AttributeError caused by accessing a non-existing default_keys attribute. --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 984f3ac57..43bfda83b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -123,7 +123,7 @@ class BaseSerializer(Field): Returns the fieldnames that should not be validated. """ excluded_fields = list(self.opts.exclude) - for field in self.fields.keys() + self.default_fields.keys(): + for field in self.fields.keys() + self.get_default_fields().keys(): if self.opts.fields: if field not in self.opts.fields + self.opts.exclude: excluded_fields.append(field) From 303bc7cf95033d2560668bf6f4d97f05f1268967 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Dec 2012 21:32:39 +0000 Subject: [PATCH 028/129] Support nullable FKs, with blank=True --- rest_framework/fields.py | 8 ++- rest_framework/serializers.py | 14 +++-- .../tests/hyperlinkedserializers.py | 25 ++++++--- rest_framework/tests/pk_relations.py | 53 +++++++++++++++++-- 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c28a9695a..bffc0fb05 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -350,7 +350,13 @@ class RelatedField(WritableField): return value = data.get(field_name) - into[(self.source or field_name)] = self.from_native(value) + + if value is None and not self.blank: + raise ValidationError('Value may not be null') + elif value is None and self.blank: + into[(self.source or field_name)] = None + else: + into[(self.source or field_name)] = self.from_native(value) class ManyRelatedMixin(object): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5edd46f5c..13c41a4bd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -431,10 +431,14 @@ class ModelSerializer(Serializer): """ # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) - queryset = model_field.rel.to._default_manager + kwargs = { + 'blank': model_field.blank, + 'queryset': model_field.rel.to._default_manager + } + if to_many: - return ManyPrimaryKeyRelatedField(queryset=queryset) - return PrimaryKeyRelatedField(queryset=queryset) + return ManyPrimaryKeyRelatedField(**kwargs) + return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): """ @@ -572,9 +576,9 @@ class HyperlinkedModelSerializer(ModelSerializer): # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) rel = model_field.rel.to - queryset = rel._default_manager kwargs = { - 'queryset': queryset, + 'blank': model_field.blank, + 'queryset': rel._default_manager, 'view_name': self._get_default_view_name(rel) } if to_many: diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index d7effce70..24bf61bf8 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -1,6 +1,7 @@ from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory +from django.utils import simplejson as json from rest_framework import generics, status, serializers from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel @@ -54,10 +55,12 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView): model = BlogPostComment serializer_class = BlogPostCommentSerializer + class BlogPostCommentDetail(generics.RetrieveAPIView): model = BlogPostComment serializer_class = BlogPostCommentSerializer + class BlogPostDetail(generics.RetrieveAPIView): model = BlogPost @@ -71,7 +74,7 @@ class AlbumDetail(generics.RetrieveAPIView): model = Album -class OptionalRelationDetail(generics.RetrieveAPIView): +class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView): model = OptionalRelationModel model_serializer_class = serializers.HyperlinkedModelSerializer @@ -162,7 +165,7 @@ class TestManyToManyHyperlinkedView(TestCase): GET requests to ListCreateAPIView should return list of objects. """ request = factory.get('/manytomany/') - response = self.list_view(request).render() + response = self.list_view(request) self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data) @@ -171,7 +174,7 @@ class TestManyToManyHyperlinkedView(TestCase): GET requests to ListCreateAPIView should return list of objects. """ request = factory.get('/manytomany/1/') - response = self.detail_view(request, pk=1).render() + response = self.detail_view(request, pk=1) self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data[0]) @@ -194,7 +197,7 @@ class TestCreateWithForeignKeys(TestCase): } request = factory.post('/comments/', data=data) - response = self.create_view(request).render() + response = self.create_view(request) 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) @@ -219,7 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase): } request = factory.post('/photos/', data=data) - response = self.list_create_view(request).render() + response = self.list_create_view(request) 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.assertEqual(self.post.photo_set.count(), 1) @@ -244,6 +247,16 @@ class TestOptionalRelationHyperlinkedView(TestCase): for non existing relations. """ request = factory.get('/optionalrelationmodel-detail/1') - response = self.detail_view(request, pk=1).render() + response = self.detail_view(request, pk=1) self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, self.data) + + def test_put_detail_view(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView with optional relations + should accept None for non existing relations. + """ + response = self.client.put('/optionalrelation/1/', + data=json.dumps(self.data), + content_type='application/json') + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py index 3dcc76f97..53245d945 100644 --- a/rest_framework/tests/pk_relations.py +++ b/rest_framework/tests/pk_relations.py @@ -49,9 +49,22 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer): model = ForeignKeySource +# Nullable ForeignKey + +class NullableForeignKeySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, + related_name='nullable_sources') + + +class NullableForeignKeySourceSerializer(serializers.ModelSerializer): + class Meta: + model = NullableForeignKeySource + + # TODO: Add test that .data cannot be accessed prior to .is_valid -class PrimaryKeyManyToManyTests(TestCase): +class PKManyToManyTests(TestCase): def setUp(self): for idx in range(1, 4): target = ManyToManyTarget(name='target-%d' % idx) @@ -137,7 +150,7 @@ class PrimaryKeyManyToManyTests(TestCase): self.assertEquals(serializer.data, expected) -class PrimaryKeyForeignKeyTests(TestCase): +class PKForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') target.save() @@ -174,7 +187,7 @@ class PrimaryKeyForeignKeyTests(TestCase): self.assertEquals(serializer.data, data) serializer.save() - # # Ensure source 1 is updated, and everything else is as expected + # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ @@ -184,6 +197,40 @@ class PrimaryKeyForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_with_invalid_null(self): + data = {'id': 1, 'name': u'source-1', 'target': None} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + +class PKNullableForeignKeyTests(TestCase): + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + for idx in range(1, 4): + source = NullableForeignKeySource(name='source-%d' % idx, target=target) + source.save() + + def test_foreign_key_update_with_valid_null(self): + data = {'id': 1, 'name': u'source-1', 'target': None} + instance = NullableForeignKeySource.objects.get(pk=1) + serializer = NullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': None}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1} + ] + self.assertEquals(serializer.data, expected) + # reverse foreign keys MUST be read_only # In the general case they do not provide .remove() or .clear() # and cannot be arbitrarily set. From c911d54ae3769243fe6c74c29b5d16c7ac6efa10 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Dec 2012 22:25:16 +0000 Subject: [PATCH 029/129] Reverted #458 When incorrect parameters are supplied to the obtain auth token view 400 *is* the correct response. --- rest_framework/authtoken/serializers.py | 2 +- rest_framework/authtoken/views.py | 5 +++-- rest_framework/tests/authentication.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index a5ed6e6d7..60a3740e7 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,6 +1,7 @@ from django.contrib.auth import authenticate from rest_framework import serializers + class AuthTokenSerializer(serializers.Serializer): username = serializers.CharField() password = serializers.CharField() @@ -21,4 +22,3 @@ class AuthTokenSerializer(serializers.Serializer): raise serializers.ValidationError('Unable to login with provided credentials.') else: raise serializers.ValidationError('Must include "username" and "password"') - diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index cfaacbe9a..d318c7233 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -6,11 +6,12 @@ from rest_framework.response import Response from rest_framework.authtoken.models import Token from rest_framework.authtoken.serializers import AuthTokenSerializer + class ObtainAuthToken(APIView): throttle_classes = () permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) - renderer_classes = (renderers.JSONRenderer,) + renderer_classes = (renderers.JSONRenderer,) model = Token def post(self, request): @@ -18,7 +19,7 @@ class ObtainAuthToken(APIView): 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_401_UNAUTHORIZED) + 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 802bc6c1f..d498ae3e0 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import patterns, include +from django.conf.urls.defaults import patterns from django.contrib.auth.models import User from django.test import Client, TestCase @@ -27,7 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,) urlpatterns = patterns('', (r'^$', MockView.as_view()), - (r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'), + (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), ) @@ -157,7 +157,7 @@ class TokenAuthTests(TestCase): 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/', + response = client.post('/auth-token/', 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) @@ -165,21 +165,21 @@ class TokenAuthTests(TestCase): 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/', + response = client.post('/auth-token/', json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') - self.assertEqual(response.status_code, 401) + 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/', + response = client.post('/auth-token/', json.dumps({'username': self.username}), 'application/json') - self.assertEqual(response.status_code, 401) + 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/', + response = client.post('/auth-token/', {'username': self.username, 'password': self.password}) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content)['token'], self.key) From 21f7dcf7c6da2e47f3b14d10018dfe6d0d060449 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Dec 2012 22:25:28 +0000 Subject: [PATCH 030/129] Added release notes --- docs/topics/release-notes.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c2fe3f64d..91cbb2fc4 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,12 @@ ## Master +* Serializers now properly support nullable Foreign Keys. +* Serializer validation now includes model field validation, such as uniqueness constraints. +* Support 'true' and 'false' string values for BooleanField. +* Added pickle support for serialized data. +* Support `source='dotted.notation'` style for nested serializers. +* Make `Request.user` settable. * Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer` ## 2.1.6 From b170973993cb269e2a061ab592d272ec9b67c86f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Dec 2012 22:36:30 +0000 Subject: [PATCH 031/129] Version 2.1.7 --- README.md | 12 ++++++++++++ docs/topics/release-notes.md | 4 +++- rest_framework/__init__.py | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f646f957f..a9ea12427 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,18 @@ To run the tests. # Changelog +## 2.1.7 + +**Date**: 7th Dec 2012 + +* Serializers now properly support nullable Foreign Keys. +* Serializer validation now includes model field validation, such as uniqueness constraints. +* Support 'true' and 'false' string values for BooleanField. +* Added pickle support for serialized data. +* Support `source='dotted.notation'` style for nested serializers. +* Make `Request.user` settable. +* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer` + ## 2.1.6 **Date**: 23rd Nov 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 91cbb2fc4..5b371d01e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,7 +4,9 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -## Master +## 2.1.7 + +**Date**: 7th Dec 2012 * Serializers now properly support nullable Foreign Keys. * Serializer validation now includes model field validation, such as uniqueness constraints. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 48cebbc5e..da2c5d5cb 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.6' +__version__ = '2.1.7' VERSION = __version__ # synonym From c1be29418b7cd8f2b44a7c3273cc79f024fa8c45 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Dec 2012 23:58:20 +0000 Subject: [PATCH 032/129] Add link to json+hal hypermedia format. --- docs/topics/rest-hypermedia-hateoas.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/rest-hypermedia-hateoas.md b/docs/topics/rest-hypermedia-hateoas.md index d76468922..10ab9dfe7 100644 --- a/docs/topics/rest-hypermedia-hateoas.md +++ b/docs/topics/rest-hypermedia-hateoas.md @@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components ## What REST framework doesn't provide. -What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope. +What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope. [cite]: http://vimeo.com/channels/restfest/page:2 [dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm @@ -44,6 +44,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format [readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list [maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html +[hal]: http://stateless.co/hal_specification.html [collection]: http://www.amundsen.com/media-types/collection/ [microformats]: http://microformats.org/wiki/Main_Page [serialization]: ../api-guide/serializers.md From f72be7b8faf4489c904bb6df97d51274872315bb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 8 Dec 2012 12:29:35 +0000 Subject: [PATCH 033/129] Add test for m2m create --- rest_framework/tests/pk_relations.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py index 53245d945..3fb3a07b9 100644 --- a/rest_framework/tests/pk_relations.py +++ b/rest_framework/tests/pk_relations.py @@ -130,6 +130,25 @@ class PKManyToManyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_many_to_many_create(self): + data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]} + serializer = ManyToManySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is added, and everything else is as expected + queryset = ManyToManySource.objects.all() + serializer = ManyToManySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'targets': [1]}, + {'id': 2, 'name': u'source-2', 'targets': [1, 2]}, + {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}, + {'id': 4, 'name': u'source-4', 'targets': [1, 3]}, + ] + self.assertEquals(serializer.data, expected) + def test_reverse_many_to_many_create(self): data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) From 936fdfb78e6987ce18812fbe3e17e2af8822704e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 8 Dec 2012 12:48:27 +0000 Subject: [PATCH 034/129] More tests for nullable FKs --- rest_framework/tests/pk_relations.py | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py index 3fb3a07b9..e3360939f 100644 --- a/rest_framework/tests/pk_relations.py +++ b/rest_framework/tests/pk_relations.py @@ -232,6 +232,49 @@ class PKNullableForeignKeyTests(TestCase): source = NullableForeignKeySource(name='source-%d' % idx, target=target) source.save() + def test_foreign_key_create_with_valid_null(self): + data = {'id': 4, 'name': u'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is created, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 1}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 4, 'name': u'source-4', 'target': None} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_create_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'id': 4, 'name': u'source-4', 'target': ''} + expected_data = {'id': 4, 'name': u'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, expected_data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is created, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 1}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 4, 'name': u'source-4', 'target': None} + ] + self.assertEquals(serializer.data, expected) + def test_foreign_key_update_with_valid_null(self): data = {'id': 1, 'name': u'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) @@ -250,6 +293,29 @@ class PKNullableForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'id': 1, 'name': u'source-1', 'target': ''} + expected_data = {'id': 1, 'name': u'source-1', 'target': None} + instance = NullableForeignKeySource.objects.get(pk=1) + serializer = NullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, expected_data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': None}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1} + ] + self.assertEquals(serializer.data, expected) + # reverse foreign keys MUST be read_only # In the general case they do not provide .remove() or .clear() # and cannot be arbitrarily set. From 733f03fba35cb13ad53723b0b15d439e40da32ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 8 Dec 2012 12:48:38 +0000 Subject: [PATCH 035/129] Fix for emptystring as nullable FK --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index bffc0fb05..c5726ff08 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -351,9 +351,9 @@ class RelatedField(WritableField): value = data.get(field_name) - if value is None and not self.blank: + if value in (None, '') and not self.blank: raise ValidationError('Value may not be null') - elif value is None and self.blank: + elif value in (None, '') and self.blank: into[(self.source or field_name)] = None else: into[(self.source or field_name)] = self.from_native(value) From ac2720afcba7be5e937b5f0360a470701861baaa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 8 Dec 2012 13:00:49 +0000 Subject: [PATCH 036/129] Add null related field option --- rest_framework/fields.py | 5 +++-- rest_framework/serializers.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c5726ff08..285ec9be1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -269,6 +269,7 @@ class RelatedField(WritableField): def __init__(self, *args, **kwargs): self.queryset = kwargs.pop('queryset', None) + self.null = kwargs.pop('null', False) super(RelatedField, self).__init__(*args, **kwargs) self.read_only = kwargs.pop('read_only', self.default_read_only) @@ -351,9 +352,9 @@ class RelatedField(WritableField): value = data.get(field_name) - if value in (None, '') and not self.blank: + if value in (None, '') and not self.null: raise ValidationError('Value may not be null') - elif value in (None, '') and self.blank: + elif value in (None, '') and self.null: into[(self.source or field_name)] = None else: into[(self.source or field_name)] = self.from_native(value) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 13c41a4bd..7eab98606 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -432,7 +432,7 @@ class ModelSerializer(Serializer): # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) kwargs = { - 'blank': model_field.blank, + 'null': model_field.null, 'queryset': model_field.rel.to._default_manager } @@ -577,7 +577,7 @@ class HyperlinkedModelSerializer(ModelSerializer): # .using(db).complex_filter(self.rel.limit_choices_to) rel = model_field.rel.to kwargs = { - 'blank': model_field.blank, + 'null': model_field.null, 'queryset': rel._default_manager, 'view_name': self._get_default_view_name(rel) } From ff01ae3571298b9da67f9b9583f0cb264676ed2b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 8 Dec 2012 13:01:03 +0000 Subject: [PATCH 037/129] Version 2.1.8 --- README.md | 7 +++++++ docs/api-guide/fields.md | 3 +++ docs/topics/release-notes.md | 9 ++++++++- rest_framework/__init__.py | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a9ea12427..a4c56103a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ To run the tests. # Changelog +## 2.1.8 + +**Date**: 8th Dec 2012 + +* Fix for creating nullable Foreign Keys with `''` as well as `None`. +* Added `null=` related field option. + ## 2.1.7 **Date**: 7th Dec 2012 diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 1d4c34cb9..50a09701e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -293,6 +293,7 @@ By default these fields are read-write, although you can change this behaviour u **Arguments**: * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. ## SlugRelatedField / ManySlugRelatedField @@ -304,6 +305,7 @@ By default these fields read-write, although you can change this behaviour using * `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. * `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. ## HyperlinkedRelatedField / ManyHyperlinkedRelatedField @@ -319,6 +321,7 @@ By default, `HyperlinkedRelatedField` is read-write, although you can change thi * `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. * `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. * `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. +* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. ## HyperLinkedIdentityField diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 5b371d01e..46eb1494d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,6 +4,13 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. +## 2.1.8 + +**Date**: 8th Dec 2012 + +* Fix for creating nullable Foreign Keys with `''` as well as `None`. +* Added `null=` related field option. + ## 2.1.7 **Date**: 7th Dec 2012 @@ -14,7 +21,7 @@ * Added pickle support for serialized data. * Support `source='dotted.notation'` style for nested serializers. * Make `Request.user` settable. -* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer` +* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`. ## 2.1.6 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index da2c5d5cb..02a606754 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.7' +__version__ = '2.1.8' VERSION = __version__ # synonym From d0935d1fbb87711b0ffda8655c44ede29ee4208a Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Mon, 10 Dec 2012 23:10:04 +0100 Subject: [PATCH 038/129] get_excluded_fieldnames() should respect Meta options' ability to be either a tuple or list. Fixes #490. Refactored `if self.opt.fields` out of the for loop. Updated and cleaned up the validation-tests. --- rest_framework/serializers.py | 6 +++--- rest_framework/tests/serializer.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7eab98606..c3f260c77 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -132,9 +132,9 @@ class BaseSerializer(Field): Returns the fieldnames that should not be validated. """ excluded_fields = list(self.opts.exclude) - for field in self.fields.keys() + self.get_default_fields().keys(): - if self.opts.fields: - if field not in self.opts.fields + self.opts.exclude: + if self.opts.fields: + for field in self.fields.keys() + self.get_default_fields().keys(): + if field not in list(self.opts.fields) + excluded_fields: excluded_fields.append(field) return excluded_fields diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 455fa270b..a16f6abd8 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -66,6 +66,7 @@ class AlbumsSerializer(serializers.ModelSerializer): class Meta: model = Album + fields = ['title'] # lists are also valid options class BasicTests(TestCase): @@ -282,9 +283,11 @@ class ValidationTests(TestCase): self.assertEquals(serializer.is_valid(), False) self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) + +class ModelValidationTests(TestCase): def test_validate_unique(self): """ - Just check if serializers.ModelSerializer.perform_model_validation() handles unique checks via .full_clean() + Just check if serializers.ModelSerializer handles unique checks via .full_clean() """ serializer = AlbumsSerializer(data={'title': 'a'}) serializer.is_valid() From 1815cdd24732e8102ccdf7d28cb5f0cc400c7eaf Mon Sep 17 00:00:00 2001 From: Venkat Date: Mon, 10 Dec 2012 17:46:21 -0800 Subject: [PATCH 039/129] Making sure the assert does not fail when required=False, read_only=True --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 285ec9be1..827459738 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -133,7 +133,7 @@ class WritableField(Field): if required is None: self.required = not(read_only) else: - assert not read_only, "Cannot set required=True and read_only=True" + assert not (read_only and required), "Cannot set required=True and read_only=True" self.required = required messages = {} From 80adaecc4307ba802fcb7e45b2f178d2102a41e9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Dec 2012 09:04:47 +0000 Subject: [PATCH 040/129] Added @annacoder. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index dfa1ee0f8..dc98dccba 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -74,6 +74,7 @@ The following people have helped make REST framework great. * Reinout van Rees - [reinout] * Michael Richards - [justanotherbody] * Ben Roberts - [roberts81] +* Venkata Subramanian Mahalingam - [annacoder] Many thanks to everyone who's contributed to the project. @@ -183,3 +184,4 @@ To contact the author directly: [reinout]: https://github.com/reinout [justanotherbody]: https://github.com/justanotherbody [roberts81]: https://github.com/roberts81 +[annacoder]: https://github.com/annacoder From 80f15c598afe138df4170ceb2198484889511d0c Mon Sep 17 00:00:00 2001 From: George Kappel Date: Tue, 11 Dec 2012 09:14:52 -0600 Subject: [PATCH 041/129] Added depth test --- rest_framework/runtests/runtests.py | 5 +++++ rest_framework/tests/serializer.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index 1bd0a5fc8..729ef26ac 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -5,6 +5,11 @@ # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py import os import sys +""" +Need to fix sys path so following works without specifically messing with PYTHONPATH +python ./rest_framework/runtests/runtests.py +""" +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' from django.conf import settings diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index a16f6abd8..f80762f0f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -726,3 +726,24 @@ class SerializerPickleTests(TestCase): model = Person fields = ('name', 'age') pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data) + +class DepthTest(TestCase): + def test_depth(self): + user = Person.objects.create(name="django",age=1) + post = BlogPost.objects.create(title="Test blog post", writer=user) + + class PersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + fields = ("name", "age") + + class BlogPostSerializer(serializers.ModelSerializer): + class Meta: + model = BlogPost + depth = 1 + + serializer = BlogPostSerializer(instance=post) + expected = {'id': 1, 'title': u'Test blog post', + 'writer': {'id': 1, 'name': u'django', 'age':1}} + + self.assertEqual(serializer.data, expected) From 17b77fc446df29e7708c210eade8369c7babc466 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Dec 2012 21:07:11 +0000 Subject: [PATCH 042/129] Added @gkrappel. Thank you! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index dc98dccba..674c83783 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -75,6 +75,7 @@ The following people have helped make REST framework great. * Michael Richards - [justanotherbody] * Ben Roberts - [roberts81] * Venkata Subramanian Mahalingam - [annacoder] +* George Kappel - [gkappel] Many thanks to everyone who's contributed to the project. @@ -185,3 +186,4 @@ To contact the author directly: [justanotherbody]: https://github.com/justanotherbody [roberts81]: https://github.com/roberts81 [annacoder]: https://github.com/annacoder +[gkappel]: https://github.com/gkappel From 405822330958c5432dde56b07a61b223c03ca4c7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Dec 2012 21:07:25 +0000 Subject: [PATCH 043/129] Fix broken nested fields --- rest_framework/compat.py | 10 ++++++++ rest_framework/fields.py | 7 ++---- rest_framework/serializers.py | 38 +++++++++++++++--------------- rest_framework/tests/serializer.py | 37 +++++++++++++++++++++-------- 4 files changed, 58 insertions(+), 34 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 09b763681..d4901437d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -19,6 +19,16 @@ except ImportError: import StringIO +# Try to import PIL in either of the two ways it can end up installed. +try: + from PIL import Image +except ImportError: + try: + import Image + except ImportError: + Image = None + + def get_concrete_model(model_cls): try: return model_cls._meta.concrete_model diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 827459738..75ce1b9f9 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1056,11 +1056,8 @@ class ImageField(FileField): 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 + from compat import Image + assert Image is not None, 'PIL must be installed for ImageField support' # We need to get a file object for PIL. We might have a path or we might # have to read the data into memory. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c3f260c77..ebeb43e83 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -237,7 +237,8 @@ class BaseSerializer(Field): except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) - # We don't run .validate() because field-validation failed and thus `attrs` may not be complete. + # If there are already errors, we don't run .validate() because + # field-validation failed and thus `attrs` may not be complete. # which in turn can cause inconsistent validation errors. if not self._errors: try: @@ -299,17 +300,14 @@ class BaseSerializer(Field): Override default so that we can apply ModelSerializer as a nested field to relationships. """ - if self.source: - value = obj for component in self.source.split('.'): - value = getattr(value, component) - if is_simple_callable(value): - value = value() - obj = value + obj = getattr(obj, component) + if is_simple_callable(obj): + obj = obj() else: - value = getattr(obj, field_name) - if is_simple_callable(value): + obj = getattr(obj, field_name) + if is_simple_callable(obj): obj = value() # If the object has an "all" method, assume it's a relationship @@ -486,15 +484,10 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def validate(self, attrs): - copied_attrs = copy.deepcopy(attrs) - restored_object = self.restore_object(copied_attrs, instance=getattr(self, 'object', None)) - self.perform_model_validation(restored_object) - return attrs - - def perform_model_validation(self, restored_object): - # Call Django's full_clean() which in turn calls: Model.clean_fields(), Model.clean(), Model.validat_unique() - restored_object.full_clean(exclude=list(self.get_excluded_fieldnames())) + # def validate(self, attrs): + # restored_object = self.restore_object(attrs, instance=getattr(self, 'object', None)) + # restored_object.full_clean(exclude=list(self.get_excluded_fieldnames())) + # return attrs def restore_object(self, attrs, instance=None): """ @@ -517,7 +510,14 @@ class ModelSerializer(Serializer): for field in self.opts.model._meta.many_to_many: if field.name in attrs: self.m2m_data[field.name] = attrs.pop(field.name) - return self.opts.model(**attrs) + + instance = self.opts.model(**attrs) + try: + instance.full_clean(exclude=list(self.get_excluded_fieldnames())) + except ValidationError, err: + self._errors = err.message_dict + return None + return instance def save(self, save_m2m=True): """ diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index f80762f0f..50a5f5a48 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,4 +1,5 @@ -import datetime, pickle +import datetime +import pickle from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, @@ -727,15 +728,11 @@ class SerializerPickleTests(TestCase): fields = ('name', 'age') pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data) -class DepthTest(TestCase): - def test_depth(self): - user = Person.objects.create(name="django",age=1) - post = BlogPost.objects.create(title="Test blog post", writer=user) - class PersonSerializer(serializers.ModelSerializer): - class Meta: - model = Person - fields = ("name", "age") +class DepthTest(TestCase): + def test_implicit_nesting(self): + writer = Person.objects.create(name="django", age=1) + post = BlogPost.objects.create(title="Test blog post", writer=writer) class BlogPostSerializer(serializers.ModelSerializer): class Meta: @@ -744,6 +741,26 @@ class DepthTest(TestCase): serializer = BlogPostSerializer(instance=post) expected = {'id': 1, 'title': u'Test blog post', - 'writer': {'id': 1, 'name': u'django', 'age':1}} + 'writer': {'id': 1, 'name': u'django', 'age': 1}} + + self.assertEqual(serializer.data, expected) + + def test_explicit_nesting(self): + writer = Person.objects.create(name="django", age=1) + post = BlogPost.objects.create(title="Test blog post", writer=writer) + + class PersonSerializer(serializers.ModelSerializer): + class Meta: + model = Person + + class BlogPostSerializer(serializers.ModelSerializer): + writer = PersonSerializer() + + class Meta: + model = BlogPost + + serializer = BlogPostSerializer(instance=post) + expected = {'id': 1, 'title': u'Test blog post', + 'writer': {'id': 1, 'name': u'django', 'age': 1}} self.assertEqual(serializer.data, expected) From 0824761f471ee5130af980acc9fdbb2758a3a92a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Dec 2012 21:07:48 +0000 Subject: [PATCH 044/129] Version 2.1.9 --- README.md | 8 ++++++++ docs/topics/release-notes.md | 8 ++++++++ rest_framework/__init__.py | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4c56103a..1bc9628f0 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,14 @@ To run the tests. # Changelog +## 2.1.9 + +**Date**: 11th Dec 2012 + +* Bugfix: Fix broken nested serialization. +* Bugfix: Fix `Meta.fields` only working as tuple not as list. +* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. + ## 2.1.8 **Date**: 8th Dec 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 46eb1494d..4f83cfd8f 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,6 +4,14 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. +## 2.1.9 + +**Date**: 11th Dec 2012 + +* Bugfix: Fix broken nested serialization. +* Bugfix: Fix `Meta.fields` only working as tuple not as list. +* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. + ## 2.1.8 **Date**: 8th Dec 2012 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 02a606754..83a6f3022 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.8' +__version__ = '2.1.9' VERSION = __version__ # synonym From 85bf4164ddef2ad6d2f58457d6621cb807ab4d29 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Dec 2012 22:09:04 +0000 Subject: [PATCH 045/129] Drop left over code --- rest_framework/serializers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ebeb43e83..5465d7b72 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -484,11 +484,6 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - # def validate(self, attrs): - # restored_object = self.restore_object(attrs, instance=getattr(self, 'object', None)) - # restored_object.full_clean(exclude=list(self.get_excluded_fieldnames())) - # return attrs - def restore_object(self, attrs, instance=None): """ Restore the model instance. From 9188d487c3a0465b2a3e0d1c47f76c3df844b7d0 Mon Sep 17 00:00:00 2001 From: Colin Murtaugh Date: Tue, 11 Dec 2012 17:26:08 -0500 Subject: [PATCH 046/129] Replaced SingleObjectBaseView with SingleObjectAPIView --- docs/tutorial/3-class-based-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index a3a18060f..b115b0229 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -109,7 +109,7 @@ The base class provides the core functionality, and the mixin classes provide th class SnippetDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, - generics.SingleObjectBaseView): + generics.SingleObjectAPIView): model = Snippet serializer_class = SnippetSerializer @@ -122,7 +122,7 @@ The base class provides the core functionality, and the mixin classes provide th def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) -Pretty similar. This time we're using the `SingleObjectBaseView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions. +Pretty similar. This time we're using the `SingleObjectAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions. ## Using generic class based views From 628e3bf001ca71da48a6f3c7bbdf209f2e20b223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 12 Dec 2012 08:59:19 +0100 Subject: [PATCH 047/129] Added @cmurtaugh. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 674c83783..cdf57f7ec 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -76,6 +76,7 @@ The following people have helped make REST framework great. * Ben Roberts - [roberts81] * Venkata Subramanian Mahalingam - [annacoder] * George Kappel - [gkappel] +* Colin Murtaugh - [cmurtaugh] Many thanks to everyone who's contributed to the project. @@ -187,3 +188,4 @@ To contact the author directly: [roberts81]: https://github.com/roberts81 [annacoder]: https://github.com/annacoder [gkappel]: https://github.com/gkappel +[cmurtaugh]: https://github.com/cmurtaugh From 497da7fc699b9e88c966e37bc48739865336683d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Dec 2012 20:44:55 +0000 Subject: [PATCH 048/129] Clean up field initialization. Fixes #497 --- rest_framework/serializers.py | 16 +++++++-- rest_framework/tests/serializer.py | 54 +++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5465d7b72..caa7c980f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -100,7 +100,8 @@ class BaseSerializer(Field): _options_class = SerializerOptions _dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations. - def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs): + def __init__(self, instance=None, data=None, files=None, + context=None, partial=False, **kwargs): super(BaseSerializer, self).__init__(**kwargs) self.opts = self._options_class(self.Meta) self.parent = None @@ -151,8 +152,6 @@ class BaseSerializer(Field): base_fields = copy.deepcopy(self.base_fields) for key, field in base_fields.items(): ret[key] = field - # Set up the field - field.initialize(parent=self, field_name=key) # Add in the default fields default_fields = self.get_default_fields() @@ -172,6 +171,10 @@ class BaseSerializer(Field): for key in self.opts.exclude: ret.pop(key, None) + # Initialize the fields + for key, field in ret.items(): + field.initialize(parent=self, field_name=key) + return ret ##### @@ -186,6 +189,13 @@ class BaseSerializer(Field): if parent.opts.depth: self.opts.depth = parent.opts.depth - 1 + # We need to call initialize here to ensure any nested + # serializers that will have already called initialize on their + # descendants get updated with *their* parent. + # We could be a bit more smart about this, but it'll do for now. + for key, field in self.fields.items(): + field.initialize(parent=self, field_name=key) + ##### # Methods to convert or revert from objects <--> primitive representations. diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 50a5f5a48..780177aa0 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,7 +4,7 @@ from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) class SubComment(object): @@ -764,3 +764,55 @@ class DepthTest(TestCase): 'writer': {'id': 1, 'name': u'django', 'age': 1}} self.assertEqual(serializer.data, expected) + + +class NestedSerializerContextTests(TestCase): + + def test_nested_serializer_context(self): + """ + Regression for #497 + + https://github.com/tomchristie/django-rest-framework/issues/497 + """ + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + fields = ("description", "callable") + + callable = serializers.SerializerMethodField('_callable') + + def _callable(self, instance): + if not 'context_item' in self.context: + raise RuntimeError("context isn't getting passed into 2nd level nested serializer") + return "success" + + class AlbumSerializer(serializers.ModelSerializer): + class Meta: + model = Album + fields = ("photo_set", "callable") + + photo_set = PhotoSerializer(source="photo_set") + callable = serializers.SerializerMethodField("_callable") + + def _callable(self, instance): + if not 'context_item' in self.context: + raise RuntimeError("context isn't getting passed into 1st level nested serializer") + return "success" + + class AlbumCollection(object): + albums = None + + class AlbumCollectionSerializer(serializers.Serializer): + albums = AlbumSerializer(source="albums") + + album1 = Album.objects.create(title="album 1") + album2 = Album.objects.create(title="album 2") + Photo.objects.create(description="Bigfoot", album=album1) + Photo.objects.create(description="Unicorn", album=album1) + Photo.objects.create(description="Yeti", album=album2) + Photo.objects.create(description="Sasquatch", album=album2) + album_collection = AlbumCollection() + album_collection.albums = [album1, album2] + + # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers + AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data From 5f08ec70e24ee3bdf74eed62def2fd9108af0eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szymon=20Te=C5=BCewski?= Date: Thu, 13 Dec 2012 12:07:56 +0100 Subject: [PATCH 049/129] context to custom field in pagination --- rest_framework/pagination.py | 2 +- rest_framework/tests/pagination.py | 31 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index d241ade7c..7d7bb647d 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -62,7 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field object_serializer = self.opts.object_serializer_class - self.fields[results_field] = object_serializer(source='object_list') + self.fields[results_field] = object_serializer(source='object_list', **kwargs) def to_native(self, obj): """ diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3062007d4..9c34001de 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -4,7 +4,7 @@ from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory from django.utils import unittest -from rest_framework import generics, status, pagination, filters +from rest_framework import generics, status, pagination, filters, serializers from rest_framework.compat import django_filters from rest_framework.tests.models import BasicModel, FilterableItem @@ -236,3 +236,32 @@ class TestCustomPaginateByParam(TestCase): response = self.view(request).render() self.assertEquals(response.data['count'], 13) self.assertEquals(response.data['results'], self.data[:5]) + + +class CustomField(serializers.Field): + def to_native(self, value): + if not 'view' in self.context: + raise RuntimeError("context isn't getting passed into custom field") + return "value" + + +class BasicModelSerializer(serializers.Serializer): + text = CustomField() + + +class TestContextPassedToCustomField(TestCase): + def setUp(self): + BasicModel.objects.create(text='ala ma kota') + + def test_with_pagination(self): + class ListView(generics.ListCreateAPIView): + model = BasicModel + serializer_class = BasicModelSerializer + paginate_by = 1 + + self.view = ListView.as_view() + request = factory.get('/') + response = self.view(request).render() + + self.assertEquals(response.status_code, status.HTTP_200_OK) + From 54d9cd4dba6b207fc8debaba1eda4d330c22e693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Thu, 13 Dec 2012 16:44:45 +0100 Subject: [PATCH 050/129] fixed validationerror usage --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 75ce1b9f9..da588082c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -581,7 +581,7 @@ class HyperlinkedRelatedField(RelatedField): except: pass - raise ValidationError('Could not resolve URL for field using view name "%s"', view_name) + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) def from_native(self, value): # Convert URL -> model instance pk @@ -680,7 +680,7 @@ class HyperlinkedIdentityField(Field): except: pass - raise ValidationError('Could not resolve URL for field using view name "%s"', view_name) + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) ##### Typed Fields ##### From 19da42822a72d4f6c65a46ff282c736a9ca8d4ea Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Thu, 13 Dec 2012 23:14:45 +0100 Subject: [PATCH 051/129] Update yaml envs to latest Django --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ccfdeacbf..0dc878373 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python: env: - DJANGO=https://github.com/django/django/zipball/master - - DJANGO=django==1.4.1 --use-mirrors - - DJANGO=django==1.3.3 --use-mirrors + - DJANGO=django==1.4.3 --use-mirrors + - DJANGO=django==1.3.5 --use-mirrors install: - pip install $DJANGO From 9eaf8e4330e0c6a4485dba650481a2578a3979b4 Mon Sep 17 00:00:00 2001 From: Simon Pantzare Date: Thu, 13 Dec 2012 17:57:27 +0100 Subject: [PATCH 052/129] Test to verify that context is passed on The paginator and its object serializer should share the same context. --- rest_framework/tests/pagination.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3062007d4..7bc23f1d2 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -4,7 +4,7 @@ from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory from django.utils import unittest -from rest_framework import generics, status, pagination, filters +from rest_framework import generics, status, pagination, filters, serializers from rest_framework.compat import django_filters from rest_framework.tests.models import BasicModel, FilterableItem @@ -148,6 +148,12 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertEquals(response.data['previous'], None) +class PassOnContextPaginationSerializer(pagination.PaginationSerializer): + + class Meta: + object_serializer_class = serializers.Serializer + + class UnitTestPagination(TestCase): """ Unit tests for pagination of primitive objects. @@ -172,6 +178,12 @@ class UnitTestPagination(TestCase): self.assertEquals(serializer.data['previous'], '?page=2') self.assertEquals(serializer.data['results'], self.objects[20:]) + def test_context_available_in_result(self): + serializer = PassOnContextPaginationSerializer(self.first_page) + results = serializer.fields[serializer.results_field] + # assertIs is available in Python 2.7 + self.assertTrue(serializer.context is results.context) + class TestUnpaginated(TestCase): """ From 39b01d6802310a90487f217c3de5428144c93429 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Dec 2012 19:59:21 +0000 Subject: [PATCH 053/129] Ensure context is passed to dynamically added fields. Fixes #476. --- rest_framework/serializers.py | 7 ++----- rest_framework/tests/pagination.py | 6 ++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index caa7c980f..8026205e5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -171,10 +171,6 @@ class BaseSerializer(Field): for key in self.opts.exclude: ret.pop(key, None) - # Initialize the fields - for key, field in ret.items(): - field.initialize(parent=self, field_name=key) - return ret ##### @@ -214,6 +210,7 @@ class BaseSerializer(Field): ret.fields = {} for field_name, field in self.fields.items(): + field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) ret[key] = value @@ -227,6 +224,7 @@ class BaseSerializer(Field): """ reverted_data = {} for field_name, field in self.fields.items(): + field.initialize(parent=self, field_name=field_name) try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: @@ -407,7 +405,6 @@ class ModelSerializer(Serializer): field = self.get_field(model_field) if field: - field.initialize(parent=self, field_name=model_field.name) ret[model_field.name] = field for field_name in self.opts.read_only_fields: diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 7bc23f1d2..3c65e9db6 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -149,7 +149,6 @@ class IntegrationTestPaginationAndFiltering(TestCase): class PassOnContextPaginationSerializer(pagination.PaginationSerializer): - class Meta: object_serializer_class = serializers.Serializer @@ -179,9 +178,12 @@ class UnitTestPagination(TestCase): self.assertEquals(serializer.data['results'], self.objects[20:]) def test_context_available_in_result(self): + """ + Ensure context gets passed through to the object serializer. + """ serializer = PassOnContextPaginationSerializer(self.first_page) + serializer.data results = serializer.fields[serializer.results_field] - # assertIs is available in Python 2.7 self.assertTrue(serializer.context is results.context) From 6f8b432677f001c46128ed6f20b2efbcf2a70feb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Dec 2012 20:08:43 +0000 Subject: [PATCH 054/129] Added @pilt. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index cdf57f7ec..e0bb366b6 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -77,6 +77,7 @@ The following people have helped make REST framework great. * Venkata Subramanian Mahalingam - [annacoder] * George Kappel - [gkappel] * Colin Murtaugh - [cmurtaugh] +* Simon Pantzare - [pilt] Many thanks to everyone who's contributed to the project. @@ -189,3 +190,4 @@ To contact the author directly: [annacoder]: https://github.com/annacoder [gkappel]: https://github.com/gkappel [cmurtaugh]: https://github.com/cmurtaugh +[pilt]: https://github.com/pilt From e9eb47207a7e599c09d9eda4e2f9adfe03ef4542 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Dec 2012 20:08:53 +0000 Subject: [PATCH 055/129] Update release notes. --- docs/topics/release-notes.md | 74 ++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4f83cfd8f..4ea393afe 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,7 +4,13 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -## 2.1.9 +## 2.1.x series + +### Master + +* Bugfix: Fix hyperlinked fields in paginated results. + +### 2.1.9 **Date**: 11th Dec 2012 @@ -12,14 +18,14 @@ * Bugfix: Fix `Meta.fields` only working as tuple not as list. * Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. -## 2.1.8 +### 2.1.8 **Date**: 8th Dec 2012 * Fix for creating nullable Foreign Keys with `''` as well as `None`. * Added `null=` related field option. -## 2.1.7 +### 2.1.7 **Date**: 7th Dec 2012 @@ -31,19 +37,19 @@ * Make `Request.user` settable. * Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`. -## 2.1.6 +### 2.1.6 **Date**: 23rd Nov 2012 * Bugfix: Unfix DjangoModelPermissions. (I am a doofus.) -## 2.1.5 +### 2.1.5 **Date**: 23rd Nov 2012 * Bugfix: Fix DjangoModelPermissions. -## 2.1.4 +### 2.1.4 **Date**: 22nd Nov 2012 @@ -54,7 +60,7 @@ * Added `obtain_token_view` to get tokens when using `TokenAuthentication`. * Bugfix: Django 1.5 configurable user support for `TokenAuthentication`. -## 2.1.3 +### 2.1.3 **Date**: 16th Nov 2012 @@ -65,14 +71,14 @@ * 201 Responses now return a 'Location' header. * Bugfix: Serializer fields now respect `max_length`. -## 2.1.2 +### 2.1.2 **Date**: 9th Nov 2012 * **Filtering support.** * Bugfix: Support creation of objects with reverse M2M relations. -## 2.1.1 +### 2.1.1 **Date**: 7th Nov 2012 @@ -82,7 +88,7 @@ * Bugfix: Make textareas same width as other fields in browsable API. * Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. -## 2.1.0 +### 2.1.0 **Date**: 5th Nov 2012 @@ -96,13 +102,17 @@ * Bugfix: Support choice field in Browseable API. * Bugfix: Related fields with `read_only=True` do not require a `queryset` argument. -## 2.0.2 +--- + +## 2.0.x series + +### 2.0.2 **Date**: 2nd Nov 2012 * Fix issues with pk related fields in the browsable API. -## 2.0.1 +### 2.0.1 **Date**: 1st Nov 2012 @@ -110,7 +120,7 @@ * Added SlugRelatedField and ManySlugRelatedField. * If PUT creates an instance return '201 Created', instead of '200 OK'. -## 2.0.0 +### 2.0.0 **Date**: 30th Oct 2012 @@ -119,7 +129,9 @@ --- -## 0.4.0 +## 0.4.x series + +### 0.4.0 * Supports Django 1.5. * Fixes issues with 'HEAD' method. @@ -131,7 +143,11 @@ * Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) * Sensible absolute URL generation, not using hacky set_script_prefix -## 0.3.3 +--- + +## 0.3.x series + +### 0.3.3 * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * Use `staticfiles` for css files. @@ -146,7 +162,7 @@ * Bugfixes: - Bug with PerUserThrottling when user contains unicode chars. -## 0.3.2 +### 0.3.2 * Bugfixes: * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115) @@ -158,37 +174,41 @@ * get_name, get_description become methods on the view - makes them overridable. * Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering -## 0.3.1 +### 0.3.1 * [not documented] -## 0.3.0 +### 0.3.0 * JSONP Support * Bugfixes, including support for latest markdown release -## 0.2.4 +--- + +## 0.2.x series + +### 0.2.4 * Fix broken IsAdminUser permission. * OPTIONS support. * XMLParser. * Drop mentions of Blog, BitBucket. -## 0.2.3 +### 0.2.3 * Fix some throttling bugs. * ``X-Throttle`` header on throttling. * Support for nesting resources on related models. -## 0.2.2 +### 0.2.2 * Throttling support complete. -## 0.2.1 +### 0.2.1 * Couple of simple bugfixes over 0.2.0 -## 0.2.0 +### 0.2.0 * Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear. The public API has been massively cleaned up. Expect it to be fairly stable from here on in. @@ -212,11 +232,15 @@ * The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin`` You can reuse these mixin classes individually without using the ``View`` class. -## 0.1.1 +--- + +## 0.1.x series + +### 0.1.1 * Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1. -## 0.1.0 +### 0.1.0 * Initial release. From 65f7aa021450f86eaa7d9ac1a061138a71d20737 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Dec 2012 20:12:50 +0000 Subject: [PATCH 056/129] Drop unneeded passing through of kwargs now context issue is resolved. --- rest_framework/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 7d7bb647d..d241ade7c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -62,7 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field object_serializer = self.opts.object_serializer_class - self.fields[results_field] = object_serializer(source='object_list', **kwargs) + self.fields[results_field] = object_serializer(source='object_list') def to_native(self, obj): """ From 1d24d1fc5928d32372e700907aa71cf887b16ba9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 14 Dec 2012 20:14:42 +0000 Subject: [PATCH 057/129] Added @sunscrapers. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e0bb366b6..ba37ce119 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -78,6 +78,7 @@ The following people have helped make REST framework great. * George Kappel - [gkappel] * Colin Murtaugh - [cmurtaugh] * Simon Pantzare - [pilt] +* Szymon Teżewski - [sunscrapers] Many thanks to everyone who's contributed to the project. @@ -191,3 +192,4 @@ To contact the author directly: [gkappel]: https://github.com/gkappel [cmurtaugh]: https://github.com/cmurtaugh [pilt]: https://github.com/pilt +[sunscrapers]: https://github.com/sunscrapers From 71ccab593b7ea7e3a1ab5dd971365c57822454ae Mon Sep 17 00:00:00 2001 From: Joel Marcotte Date: Sat, 15 Dec 2012 10:35:06 -0500 Subject: [PATCH 058/129] Fix for JSON integer match to a ChoiceField --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index da588082c..903c384e3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -794,7 +794,7 @@ class ChoiceField(WritableField): if value == smart_unicode(k2): return True else: - if value == smart_unicode(k): + if value == smart_unicode(k) or value == k: return True return False From 35f72cecb199e1790a42fadd6037da43cdd5a05a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 15 Dec 2012 20:40:41 +0000 Subject: [PATCH 059/129] Fix model validation exclusions. Fixes #500. Fixes #506. --- docs/topics/release-notes.md | 1 + rest_framework/fields.py | 1 + rest_framework/serializers.py | 25 +++++++++++++------------ rest_framework/tests/models.py | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4ea393afe..6d7dc3485 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -8,6 +8,7 @@ ### Master +* Bugfix: Ensure read-only fields don't have model validation applied. * Bugfix: Fix hyperlinked fields in paginated results. ### 2.1.9 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index da588082c..d3ef8f779 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -32,6 +32,7 @@ def is_simple_callable(obj): class Field(object): + read_only = True creation_counter = 0 empty = '' type_name = None diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8026205e5..1d93f7775 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -128,17 +128,6 @@ class BaseSerializer(Field): """ return {} - def get_excluded_fieldnames(self): - """ - Returns the fieldnames that should not be validated. - """ - excluded_fields = list(self.opts.exclude) - if self.opts.fields: - for field in self.fields.keys() + self.get_default_fields().keys(): - if field not in list(self.opts.fields) + excluded_fields: - excluded_fields.append(field) - return excluded_fields - def get_fields(self): """ Returns the complete set of fields for the object as a dict. @@ -491,6 +480,18 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) + def get_validation_exclusions(self): + """ + Return a list of field names to exclude from model validation. + """ + cls = self.opts.model + opts = get_concrete_model(cls)._meta + exclusions = [field.name for field in opts.fields + opts.many_to_many] + for field_name, field in self.fields.items(): + if field_name in exclusions and not field.read_only: + exclusions.remove(field_name) + return exclusions + def restore_object(self, attrs, instance=None): """ Restore the model instance. @@ -515,7 +516,7 @@ class ModelSerializer(Serializer): instance = self.opts.model(**attrs) try: - instance.full_clean(exclude=list(self.get_excluded_fieldnames())) + instance.full_clean(exclude=self.get_validation_exclusions()) except ValidationError, err: self._errors = err.message_dict return None diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 428bf130d..e2b287d06 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -61,7 +61,7 @@ class BasicModel(RESTFrameworkModel): class SlugBasedModel(RESTFrameworkModel): text = models.CharField(max_length=100) - slug = models.SlugField(max_length=32, blank=True) + slug = models.SlugField(max_length=32) class DefaultValueModel(RESTFrameworkModel): From 01e06bcdf8a4678a312acbf11638fa6a033c50d6 Mon Sep 17 00:00:00 2001 From: Joel Marcotte Date: Sat, 15 Dec 2012 16:33:08 -0500 Subject: [PATCH 060/129] Added test for "positive_integer in choices tuple does not get parsed if not string". Signed-off-by: Joel Marcotte --- rest_framework/tests/models.py | 4 ++++ rest_framework/tests/serializer.py | 13 ++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 428bf130d..807bcf983 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -51,6 +51,10 @@ class RESTFrameworkModel(models.Model): abstract = True +class HasPositiveIntegerAsChoice(RESTFrameworkModel): + some_choices = ((1,'A'),(2,'B'),(3,'C')) + some_integer = models.PositiveIntegerField(choices=some_choices) + class Anchor(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 780177aa0..7f2c27b05 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -2,7 +2,7 @@ import datetime import pickle from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, +from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) @@ -69,6 +69,11 @@ class AlbumsSerializer(serializers.ModelSerializer): model = Album fields = ['title'] # lists are also valid options +class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): + class Meta: + model = HasPositiveIntegerAsChoice + fields = ['some_integer'] + class BasicTests(TestCase): def setUp(self): @@ -285,6 +290,12 @@ class ValidationTests(TestCase): self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) +class PositiveIntegerAsChoiceTests(TestCase): + def test_positive_integer_in_json_is_correctly_parsed(self): + data = {'some_integer':1} + serializer = PositiveIntegerAsChoiceSerializer(data=data) + self.assertEquals(serializer.is_valid(), True) + class ModelValidationTests(TestCase): def test_validate_unique(self): """ From 6f25181979084e769658748ea342ff088ad245c0 Mon Sep 17 00:00:00 2001 From: Joel Marcotte Date: Sat, 15 Dec 2012 16:45:04 -0500 Subject: [PATCH 061/129] Reverting commit to previous state to see if the test is only relevant to django 1.5b2 --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 903c384e3..da588082c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -794,7 +794,7 @@ class ChoiceField(WritableField): if value == smart_unicode(k2): return True else: - if value == smart_unicode(k) or value == k: + if value == smart_unicode(k): return True return False From 262d9c248918d1e9a2e6ee8008aca94e2e23dd82 Mon Sep 17 00:00:00 2001 From: Joel Marcotte Date: Sat, 15 Dec 2012 16:52:28 -0500 Subject: [PATCH 062/129] Final commit to restore the fix Signed-off-by: Joel Marcotte --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index da588082c..903c384e3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -794,7 +794,7 @@ class ChoiceField(WritableField): if value == smart_unicode(k2): return True else: - if value == smart_unicode(k): + if value == smart_unicode(k) or value == k: return True return False From 008dafce178181855d66981cfacb908d013c5d1d Mon Sep 17 00:00:00 2001 From: toran billups Date: Sat, 15 Dec 2012 20:55:36 -0600 Subject: [PATCH 063/129] ManyPrimaryKeyRelatedField now supports create for one-to-many rel --- rest_framework/serializers.py | 12 +++++ rest_framework/tests/models.py | 5 ++ rest_framework/tests/serializer.py | 79 ++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8026205e5..276a7db79 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -496,12 +496,19 @@ class ModelSerializer(Serializer): Restore the model instance. """ self.m2m_data = {} + self.related_data = {} if instance is not None: for key, val in attrs.items(): setattr(instance, key, val) return instance + # Related relations + for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model(): + field_name = obj.field.related_query_name() + if field_name in attrs: + self.related_data[field_name] = attrs.pop(field_name) + # Reverse relations for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): field_name = obj.field.related_query_name() @@ -532,6 +539,11 @@ class ModelSerializer(Serializer): setattr(self.object, accessor_name, object_list) self.m2m_data = {} + if getattr(self, 'related_data', None): + for accessor_name, object_list in self.related_data.items(): + setattr(self.object, accessor_name, object_list) + self.related_data = {} + return self.object diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 428bf130d..0aa00d764 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -149,6 +149,11 @@ class BlogPostComment(RESTFrameworkModel): blog_post = models.ForeignKey(BlogPost) +class BlogPostRelatedComment(RESTFrameworkModel): + text = models.TextField() + blog_post = models.ForeignKey(BlogPost, related_name="comments") + + class Album(RESTFrameworkModel): title = models.CharField(max_length=100, unique=True) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 780177aa0..3c56f127f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -816,3 +816,82 @@ class NestedSerializerContextTests(TestCase): # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data + + +class ManyPrimaryKeyRelatedCreateTest(TestCase): + + def test_create_is_valid_with_title_and_empty_comments_list(self): + data = {'title': 'foobar', 'comments': []} + serializer = self.build_model_serializer(data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_is_valid_with_title_and_comment(self): + data = {'title': 'foobar', 'comments': [self.comment.pk]} + serializer = self.build_model_serializer(data) + self.assertEquals(serializer.is_valid(), True) + + def test_create_is_not_valid_when_title_is_empty_string(self): + data = {'title': '', 'comments': [self.comment.pk]} + serializer = self.build_model_serializer(data) + self.assertEquals(serializer.is_valid(), False) + self.assertEquals(serializer.errors, {'title': [u'This field is required.']}) + + def test_create_is_not_valid_when_title_present_but_no_comments(self): + data = {'title': 'foobar'} + serializer = self.build_model_serializer(data) + try: + self.assertEquals(serializer.is_valid(), False) + except TypeError as e: + self.assertEqual(e.message, "'NoneType' object is not iterable") + + def test_create_without_comment_returns_expected_json_result(self): + data = {'title': 'foobar', 'comments': []} + serializer = self.build_model_serializer(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + expected = { + 'title': u'foobar', + 'comments': [] + } + self.assertEqual(serializer.data, expected) + + def test_create_with_comment_returns_expected_json_result(self): + data = {'title': 'foobar', 'comments': [self.comment.pk]} + serializer = self.build_model_serializer(data) + self.assertEquals(serializer.is_valid(), True) + instance = serializer.save() + expected = { + 'title': u'foobar', + 'comments': [self.comment.pk] + } + self.assertEqual(serializer.data, expected) + + @property + def comment(self): + if not hasattr(self, '_comment'): + from rest_framework.tests.models import BlogPostRelatedComment + self._comment = BlogPostRelatedComment.objects.create(text="I love this blog post", blog_post=self.post) + return self._comment + + @property + def post(self): + if not hasattr(self, '_post'): + from rest_framework.tests.models import BlogPost + self._post = BlogPost.objects.create(title="Test blog post") + return self._post + + def build_model_serializer(self, data): + from rest_framework.tests.models import BlogPost, BlogPostRelatedComment + + class BlogPostRelatedCommentSerializer(serializers.ModelSerializer): + class Meta: + model = BlogPostRelatedComment + fields = ("text") + + class BlogPostSerializer(serializers.ModelSerializer): + comments = serializers.ManyPrimaryKeyRelatedField() + class Meta: + model = BlogPost + fields = ("title", "comments") + + return BlogPostSerializer(data=data) From 70714c234630cd205ed88686ece3b594f387a48f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Dec 2012 09:08:28 +0000 Subject: [PATCH 064/129] Version 2.1.10 --- README.md | 7 +++++++ docs/topics/release-notes.md | 4 +++- rest_framework/__init__.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1bc9628f0..1d49d3be6 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ To run the tests. # Changelog +## 2.1.10 + +**Date**: 17th Dec 2012 + +* Bugfix: Ensure read-only fields don't have model validation applied. +* Bugfix: Fix hyperlinked fields in paginated results. + ## 2.1.9 **Date**: 11th Dec 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 6d7dc3485..66d6f7f38 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,7 +6,9 @@ ## 2.1.x series -### Master +### 2.1.10 + +**Date**: 17th Dec 2012 * Bugfix: Ensure read-only fields don't have model validation applied. * Bugfix: Fix hyperlinked fields in paginated results. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 83a6f3022..d61632bcf 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.9' +__version__ = '2.1.10' VERSION = __version__ # synonym From 6f6aeadf5f66f1ca788f7e7c6375d64dbfb92b00 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 17 Dec 2012 16:31:18 +0200 Subject: [PATCH 065/129] CharField in model should be null=True according Django docs All tests should still pass Ref #514 --- rest_framework/tests/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 428bf130d..3a939a248 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -160,7 +160,7 @@ class Photo(RESTFrameworkModel): # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=True) + title = models.CharField(max_length=100, blank=True, null=False) # Model for issue #380 From 967f22e7d1d7feef565d32ce67401d13792cbe2b Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 17 Dec 2012 16:49:45 +0200 Subject: [PATCH 066/129] Failing test case for #514 Serializer errors are: {'title': [u'This field is required.']} --- rest_framework/tests/serializer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 780177aa0..7b05d2596 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -704,6 +704,10 @@ class BlankFieldTests(TestCase): serializer = self.not_blank_model_serializer_class(data=self.data) self.assertEquals(serializer.is_valid(), False) + def test_create_model_null_field(self): + serializer = self.model_serializer_class(data={}) + self.assertEquals(serializer.is_valid(), True) + #test for issue #460 class SerializerPickleTests(TestCase): From c68f7ca580989a76d335e8a12debd63f85636149 Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Mon, 17 Dec 2012 16:57:11 +0200 Subject: [PATCH 067/129] blank=True fields are not required - fixes #514 --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index caa7c980f..9c53a17af 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -456,7 +456,7 @@ class ModelSerializer(Serializer): kwargs['blank'] = model_field.blank - if model_field.null: + if model_field.null or model_field.blank: kwargs['required'] = False if model_field.has_default(): From aa72f8d63d2a7b33a2e74eaba216b56c803af70c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Dec 2012 21:59:51 +0000 Subject: [PATCH 068/129] Fix bug with M2M in browseable API --- rest_framework/renderers.py | 2 +- rest_framework/serializers.py | 10 +++------- rest_framework/tests/generics.py | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1220bca10..a4ae717db 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,7 +20,7 @@ from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework import VERSION, status -from rest_framework import serializers, parsers +from rest_framework import parsers class BaseRenderer(object): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1d93f7775..02377492c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -160,6 +160,9 @@ class BaseSerializer(Field): for key in self.opts.exclude: ret.pop(key, None) + for key, field in ret.items(): + field.initialize(parent=self, field_name=key) + return ret ##### @@ -174,13 +177,6 @@ class BaseSerializer(Field): if parent.opts.depth: self.opts.depth = parent.opts.depth - 1 - # We need to call initialize here to ensure any nested - # serializers that will have already called initialize on their - # descendants get updated with *their* parent. - # We could be a bit more smart about this, but it'll do for now. - for key, field in self.fields.items(): - field.initialize(parent=self, field_name=key) - ##### # Methods to convert or revert from objects <--> primitive representations. diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a8279ef2b..14fa66f93 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,3 +1,4 @@ +from django.db import models from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json @@ -301,3 +302,36 @@ class TestCreateModelWithAutoNowAddField(TestCase): self.assertEquals(response.status_code, status.HTTP_201_CREATED) created = self.objects.get(id=1) self.assertEquals(created.content, 'foobar') + + +# Test for particularly ugly reression with m2m in browseable API +class ClassB(models.Model): + name = models.CharField(max_length=255) + + +class ClassA(models.Model): + name = models.CharField(max_length=255) + childs = models.ManyToManyField(ClassB, blank=True, null=True) + + +class ClassASerializer(serializers.ModelSerializer): + childs = serializers.ManyPrimaryKeyRelatedField(source='childs') + + class Meta: + model = ClassA + + +class ExampleView(generics.ListCreateAPIView): + serializer_class = ClassASerializer + model = ClassA + + +class TestM2MBrowseableAPI(TestCase): + def test_m2m_in_browseable_api(self): + """ + Test for particularly ugly reression with m2m in browseable API + """ + request = factory.get('/', HTTP_ACCEPT='text/html') + view = ExampleView().as_view() + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) From 0418cebc583711263d983e0e3acbd474eb6f0fd2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Dec 2012 22:00:25 +0000 Subject: [PATCH 069/129] Update contact details --- README.md | 5 ++++- docs/topics/credits.md | 6 +++--- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1d49d3be6..931cd6539 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ **A toolkit for building well-connected, self-describing web APIs.** -**Author:** Tom Christie. [Follow me on Twitter][twitter] +**Author:** Tom Christie. [Follow me on Twitter][twitter]. + +**Support:** [REST framework discussion group][group]. [![build-status-image]][travis] @@ -205,6 +207,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2 [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [twitter]: https://twitter.com/_tomchristie +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [sandbox]: http://restframework.herokuapp.com/ [rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html diff --git a/docs/topics/credits.md b/docs/topics/credits.md index ba37ce119..c169fd74e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -98,10 +98,9 @@ Development of REST framework 2.0 was sponsored by [DabApps]. ## Contact -To contact the author directly: +For usage questions please see the [REST framework discussion group][group]. -* twitter: [@_tomchristie][twitter] -* email: [tom@tomchristie.com][email] +You can also contact [@_tomchristie][twitter] directly on twitter. [email]: mailto:tom@tomchristie.com [twitter]: http://twitter.com/_tomchristie @@ -115,6 +114,7 @@ To contact the author directly: [dabapps]: http://lab.dabapps.com [sandbox]: http://restframework.herokuapp.com/ [heroku]: http://www.heroku.com/ +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [tomchristie]: https://github.com/tomchristie [markotibold]: https://github.com/markotibold diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index b5d378751..216ca4338 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -163,9 +163,9 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: -* Contribute on [GitHub][github] by reviewing and subitting issues, and making pull requests. +* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. * Join the [REST framework discussion group][group], and help build the community. -* Follow the author [on Twitter][twitter] and say hi. +* [Follow the author on Twitter][twitter] and say hi. **Now go build awesome things.** From 9680b42b506a47df1cac83107aff15bfd4a99a48 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Dec 2012 22:01:17 +0000 Subject: [PATCH 070/129] Version 2.1.11 --- README.md | 6 ++++++ docs/topics/release-notes.md | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/README.md b/README.md index 931cd6539..b4078b15c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ To run the tests. # Changelog +## 2.1.11 + +**Date**: 17th Dec 2012 + +* Bugfix: Fix issue with M2M fields in browseable API. + ## 2.1.10 **Date**: 17th Dec 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 66d6f7f38..a9083bb4d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,12 @@ ## 2.1.x series +### 2.1.11 + +**Date**: 17th Dec 2012 + +* Bugfix: Fix issue with M2M fields in browseable API. + ### 2.1.10 **Date**: 17th Dec 2012 From 8f23b7f2f901751ecb79e98a540d3a8dc83b0d1a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Dec 2012 22:01:50 +0000 Subject: [PATCH 071/129] Version 2.1.11 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index d61632bcf..d5cac5c6d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.10' +__version__ = '2.1.11' VERSION = __version__ # synonym From 6693d2d277823c9150077c7c9534b7550dfd192c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Dec 2012 18:21:58 +0000 Subject: [PATCH 072/129] Fix for pks returning as strings when set in pre_save. Fixes #482. Thanks to @n8agrin for the bug report. --- rest_framework/mixins.py | 4 ++++ rest_framework/serializers.py | 24 +++++++++++++----------- rest_framework/tests/generics.py | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 1edcfa5c9..8dc0c3295 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -113,6 +113,10 @@ class UpdateModelMixin(object): slug_field = self.get_slug_field() setattr(obj, slug_field, slug) + # Ensure we clean the attributes so that we don't eg return integer + # pk using a string representation, as provided by the url conf kwarg. + obj.full_clean() + class DestroyModelMixin(object): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 02377492c..8156bc185 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -497,25 +497,27 @@ class ModelSerializer(Serializer): if instance is not None: for key, val in attrs.items(): setattr(instance, key, val) - return instance - # Reverse relations - for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): - field_name = obj.field.related_query_name() - if field_name in attrs: - self.m2m_data[field_name] = attrs.pop(field_name) + else: + # Reverse relations + for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): + field_name = obj.field.related_query_name() + if field_name in attrs: + self.m2m_data[field_name] = attrs.pop(field_name) - # Forward relations - for field in self.opts.model._meta.many_to_many: - if field.name in attrs: - self.m2m_data[field.name] = attrs.pop(field.name) + # Forward relations + for field in self.opts.model._meta.many_to_many: + if field.name in attrs: + self.m2m_data[field.name] = attrs.pop(field.name) + + instance = self.opts.model(**attrs) - instance = self.opts.model(**attrs) try: instance.full_clean(exclude=self.get_validation_exclusions()) except ValidationError, err: self._errors = err.message_dict return None + return instance def save(self, save_m2m=True): diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 14fa66f93..7c24d84ea 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -175,7 +175,7 @@ class TestInstanceView(TestCase): content = {'text': 'foobar'} request = factory.put('/1', json.dumps(content), content_type='application/json') - response = self.view(request, pk=1).render() + response = self.view(request, pk='1').render() self.assertEquals(response.status_code, status.HTTP_200_OK) self.assertEquals(response.data, {'id': 1, 'text': 'foobar'}) updated = self.objects.get(id=1) From 24581faceb65d78acecd253808581a5534862d08 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Dec 2012 18:23:28 +0000 Subject: [PATCH 073/129] Update release notes. --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index a9083bb4d..87aaefbdb 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,10 @@ ## 2.1.x series +### Master + +* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. + ### 2.1.11 **Date**: 17th Dec 2012 From 6611514134b9e48f7e08322b42870eae1b5e8e6d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Dec 2012 19:20:10 +0000 Subject: [PATCH 074/129] Whitespace --- rest_framework/fields.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d3ef8f779..2060e3e70 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -384,6 +384,7 @@ class ManyRelatedMixin(object): else: if value == ['']: value = [] + into[field_name] = [self.from_native(item) for item in value] From ab86990ac8a735aa3ba4d858040d7ea022321b46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Dec 2012 19:20:31 +0000 Subject: [PATCH 075/129] HyperlinkedRealtedField tests. Refs #442. --- rest_framework/tests/hyperlink_relations.py | 358 ++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 rest_framework/tests/hyperlink_relations.py diff --git a/rest_framework/tests/hyperlink_relations.py b/rest_framework/tests/hyperlink_relations.py new file mode 100644 index 000000000..8f0238737 --- /dev/null +++ b/rest_framework/tests/hyperlink_relations.py @@ -0,0 +1,358 @@ +from django.conf.urls import patterns, url +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +def dummy_view(request, pk): + pass + +urlpatterns = patterns('', + url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'), + url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), + url(r'^foreignkeysource/(?P[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), + url(r'^foreignkeytarget/(?P[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'), + url(r'^nullableforeignkeysource/(?P[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'), +) + + +# ManyToMany + +class ManyToManyTarget(models.Model): + name = models.CharField(max_length=100) + + +class ManyToManySource(models.Model): + name = models.CharField(max_length=100) + targets = models.ManyToManyField(ManyToManyTarget, related_name='sources') + + +class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer): + sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail') + + class Meta: + model = ManyToManyTarget + + +class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ManyToManySource + + +# ForeignKey + +class ForeignKeyTarget(models.Model): + name = models.CharField(max_length=100) + + +class ForeignKeySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, related_name='sources') + + +class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer): + sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail', read_only=True) + + class Meta: + model = ForeignKeyTarget + + +class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ForeignKeySource + + +# Nullable ForeignKey + +class NullableForeignKeySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, + related_name='nullable_sources') + + +class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = NullableForeignKeySource + + +# TODO: Add test that .data cannot be accessed prior to .is_valid + +class HyperlinkedManyToManyTests(TestCase): + urls = 'rest_framework.tests.hyperlink_relations' + + def setUp(self): + for idx in range(1, 4): + target = ManyToManyTarget(name='target-%d' % idx) + target.save() + source = ManyToManySource(name='source-%d' % idx) + source.save() + for target in ManyToManyTarget.objects.all(): + source.targets.add(target) + + def test_many_to_many_retrieve(self): + queryset = ManyToManySource.objects.all() + serializer = ManyToManySourceSerializer(queryset) + expected = [ + {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, + {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_many_to_many_retrieve(self): + queryset = ManyToManyTarget.objects.all() + serializer = ManyToManyTargetSerializer(queryset) + expected = [ + {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} + ] + self.assertEquals(serializer.data, expected) + + def test_many_to_many_update(self): + data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + instance = ManyToManySource.objects.get(pk=1) + serializer = ManyToManySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = ManyToManySource.objects.all() + serializer = ManyToManySourceSerializer(queryset) + expected = [ + {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, + {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_many_to_many_update(self): + data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']} + instance = ManyToManyTarget.objects.get(pk=1) + serializer = ManyToManyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 1 is updated, and everything else is as expected + queryset = ManyToManyTarget.objects.all() + serializer = ManyToManyTargetSerializer(queryset) + expected = [ + {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}, + {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} + + ] + self.assertEquals(serializer.data, expected) + + def test_many_to_many_create(self): + data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} + serializer = ManyToManySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is added, and everything else is as expected + queryset = ManyToManySource.objects.all() + serializer = ManyToManySourceSerializer(queryset) + expected = [ + {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, + {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, + {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, + {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_many_to_many_create(self): + data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} + serializer = ManyToManyTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'target-4') + + # Ensure target 4 is added, and everything else is as expected + queryset = ManyToManyTarget.objects.all() + serializer = ManyToManyTargetSerializer(queryset) + expected = [ + {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, + {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}, + {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} + ] + self.assertEquals(serializer.data, expected) + + +class HyperlinkedForeignKeyTests(TestCase): + urls = 'rest_framework.tests.hyperlink_relations' + + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + new_target = ForeignKeyTarget(name='target-2') + new_target.save() + for idx in range(1, 4): + source = ForeignKeySource(name='source-%d' % idx, target=target) + source.save() + + def test_foreign_key_retrieve(self): + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_retrieve(self): + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update(self): + data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'}, + {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_with_invalid_null(self): + data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + +class HyperlinkedNullableForeignKeyTests(TestCase): + urls = 'rest_framework.tests.hyperlink_relations' + + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + for idx in range(1, 4): + source = NullableForeignKeySource(name='source-%d' % idx, target=target) + source.save() + + def test_foreign_key_create_with_valid_null(self): + data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is created, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_create_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''} + expected_data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + serializer = NullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, expected_data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 4 is created, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_with_valid_null(self): + data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} + instance = NullableForeignKeySource.objects.get(pk=1) + serializer = NullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, + {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''} + expected_data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} + instance = NullableForeignKeySource.objects.get(pk=1) + serializer = NullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, expected_data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, + {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + ] + self.assertEquals(serializer.data, expected) + + # reverse foreign keys MUST be read_only + # In the general case they do not provide .remove() or .clear() + # and cannot be arbitrarily set. + + # def test_reverse_foreign_key_update(self): + # data = {'id': 1, 'name': u'target-1', 'sources': [1]} + # instance = ForeignKeyTarget.objects.get(pk=1) + # serializer = ForeignKeyTargetSerializer(instance, data=data) + # self.assertTrue(serializer.is_valid()) + # self.assertEquals(serializer.data, data) + # serializer.save() + + # # Ensure target 1 is updated, and everything else is as expected + # queryset = ForeignKeyTarget.objects.all() + # serializer = ForeignKeyTargetSerializer(queryset) + # expected = [ + # {'id': 1, 'name': u'target-1', 'sources': [1]}, + # {'id': 2, 'name': u'target-2', 'sources': []}, + # ] + # self.assertEquals(serializer.data, expected) From 24e14b7d53e43f1574971ff5b6eee6d0185df23a Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 14 Nov 2012 12:42:30 -0800 Subject: [PATCH 076/129] Add tests for retrieving/updating reverse fks --- rest_framework/tests/nested_relations.py | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 rest_framework/tests/nested_relations.py diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py new file mode 100644 index 000000000..35e75bb03 --- /dev/null +++ b/rest_framework/tests/nested_relations.py @@ -0,0 +1,68 @@ +from copy import deepcopy +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +# ForeignKey + +class ForeignKeyTarget(models.Model): + name = models.CharField(max_length=100) + + +class ForeignKeySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, related_name='sources') + + +class ForeignKeySourceSerializer(serializers.ModelSerializer): + class Meta: + model = ForeignKeySource + + +class ForeignKeyTargetSerializer(serializers.ModelSerializer): + sources = ForeignKeySourceSerializer() + + class Meta: + model = ForeignKeyTarget + + +class ReverseForeignKeyTests(TestCase): + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + new_target = ForeignKeyTarget(name='target-2') + new_target.save() + for idx in range(1, 4): + source = ForeignKeySource(name='source-%d' % idx, target=target) + source.save() + self.target_data = {'id': 1, 'name': u'target-1', 'sources': [ + {'id': 1, 'name': u'source-1', 'target': 1}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1}, + ]} + self.new_target_data = {'id': 2, 'name': u'target-2', 'sources': []} + self.data = [self.target_data, self.new_target_data] + + def test_reverse_foreign_key_retrieve(self): + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + self.assertEquals(serializer.data, self.data) + + def test_reverse_foreign_key_update(self): + data = deepcopy(self.target_data) + data['sources'][0]['name'] = 'source-1-changed' + data['sources'][2]['name'] = 'source-3-changed' + instance = ForeignKeyTarget.objects.get(pk=1) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 1 is updated, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = deepcopy(self.data) + expected[0]['sources'][0]['name'] = 'source-1-changed' + expected[0]['sources'][2]['name'] = 'source-3-changed' + self.assertEquals(serializer.data, expected) From f92c5b28ade6d01f0b52fe59bbc8cdbf44080e92 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 14 Nov 2012 13:39:54 -0800 Subject: [PATCH 077/129] Add test for creating a reverse fk relation --- rest_framework/tests/nested_relations.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py index 35e75bb03..b5daea891 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/nested_relations.py @@ -49,6 +49,22 @@ class ReverseForeignKeyTests(TestCase): serializer = ForeignKeyTargetSerializer(queryset) self.assertEquals(serializer.data, self.data) + def test_reverse_foreign_key_create(self): + data = deepcopy(self.new_target_data) + data['sources'].append({'name': u'source-4', 'target': 2}) + instance = ForeignKeyTarget.objects.get(pk=2) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 2 has new source and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = deepcopy(self.data) + expected[1]['sources'].append({'id': 4, 'name': 'source-4', 'target': 2}) + self.assertEquals(serializer.data, expected) + def test_reverse_foreign_key_update(self): data = deepcopy(self.target_data) data['sources'][0]['name'] = 'source-1-changed' From cbf342900515859f4322d273a7ef8988a119f507 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 14 Nov 2012 15:15:35 -0800 Subject: [PATCH 078/129] Add test for deleting a reverse fk relation --- rest_framework/tests/nested_relations.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py index b5daea891..93d95622c 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/nested_relations.py @@ -82,3 +82,19 @@ class ReverseForeignKeyTests(TestCase): expected[0]['sources'][0]['name'] = 'source-1-changed' expected[0]['sources'][2]['name'] = 'source-3-changed' self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_delete(self): + data = deepcopy(self.target_data) + del data['sources'][2] + instance = ForeignKeyTarget.objects.get(pk=1) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 1 has 2 sources and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = deepcopy(self.data) + del expected[0]['sources'][2] + self.assertEquals(serializer.data, expected) From 2910bfb5275c88d30aa73e580a35a46684177d38 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Wed, 14 Nov 2012 15:27:38 -0800 Subject: [PATCH 079/129] Add two functions for more DRY reverse fk tests --- rest_framework/tests/nested_relations.py | 44 ++++++++++-------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py index 93d95622c..c88bd2b3a 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/nested_relations.py @@ -44,57 +44,51 @@ class ReverseForeignKeyTests(TestCase): self.new_target_data = {'id': 2, 'name': u'target-2', 'sources': []} self.data = [self.target_data, self.new_target_data] - def test_reverse_foreign_key_retrieve(self): - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) - self.assertEquals(serializer.data, self.data) - - def test_reverse_foreign_key_create(self): - data = deepcopy(self.new_target_data) - data['sources'].append({'name': u'source-4', 'target': 2}) - instance = ForeignKeyTarget.objects.get(pk=2) + def save_serialized_target(self, instance, data): serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) self.assertEquals(serializer.data, data) serializer.save() - # Ensure target 2 has new source and everything else is as expected + def check_serialized_targets(self, data): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) + self.assertEquals(serializer.data, data) + + def test_reverse_foreign_key_retrieve(self): + self.check_serialized_targets(self.data) + + def test_reverse_foreign_key_create(self): + data = deepcopy(self.new_target_data) + data['sources'].append({'name': u'source-4', 'target': 2}) + instance = ForeignKeyTarget.objects.get(pk=2) + self.save_serialized_target(instance, data) + + # Ensure target 2 has new source and everything else is as expected expected = deepcopy(self.data) expected[1]['sources'].append({'id': 4, 'name': 'source-4', 'target': 2}) - self.assertEquals(serializer.data, expected) + self.check_serialized_targets(expected) def test_reverse_foreign_key_update(self): data = deepcopy(self.target_data) data['sources'][0]['name'] = 'source-1-changed' data['sources'][2]['name'] = 'source-3-changed' instance = ForeignKeyTarget.objects.get(pk=1) - serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) - serializer.save() + self.save_serialized_target(instance, data) # Ensure target 1 is updated, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) expected = deepcopy(self.data) expected[0]['sources'][0]['name'] = 'source-1-changed' expected[0]['sources'][2]['name'] = 'source-3-changed' - self.assertEquals(serializer.data, expected) + self.check_serialized_targets(expected) def test_reverse_foreign_key_delete(self): data = deepcopy(self.target_data) del data['sources'][2] instance = ForeignKeyTarget.objects.get(pk=1) - serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) - serializer.save() + self.save_serialized_target(instance, data) # Ensure target 1 has 2 sources and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) expected = deepcopy(self.data) del expected[0]['sources'][2] - self.assertEquals(serializer.data, expected) + self.check_serialized_targets(expected) From 8a41d4aa5411560aabc5c198976b7df6580e6143 Mon Sep 17 00:00:00 2001 From: Trey Hunner Date: Fri, 16 Nov 2012 23:01:03 -0800 Subject: [PATCH 080/129] Fix assertion for nested create test (missing id) --- rest_framework/tests/nested_relations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py index c88bd2b3a..0d4d6ce89 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/nested_relations.py @@ -47,7 +47,6 @@ class ReverseForeignKeyTests(TestCase): def save_serialized_target(self, instance, data): serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() def check_serialized_targets(self, data): From c6a6d7ac15b351e2d81685abefd44542367b6e6f Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Wed, 19 Dec 2012 07:33:49 -0800 Subject: [PATCH 081/129] remove all but the 'read' nested serializer tests --- rest_framework/tests/nested_relations.py | 40 ------------------------ 1 file changed, 40 deletions(-) diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/nested_relations.py index 0d4d6ce89..b9022cac2 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/nested_relations.py @@ -44,11 +44,6 @@ class ReverseForeignKeyTests(TestCase): self.new_target_data = {'id': 2, 'name': u'target-2', 'sources': []} self.data = [self.target_data, self.new_target_data] - def save_serialized_target(self, instance, data): - serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - serializer.save() - def check_serialized_targets(self, data): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) @@ -56,38 +51,3 @@ class ReverseForeignKeyTests(TestCase): def test_reverse_foreign_key_retrieve(self): self.check_serialized_targets(self.data) - - def test_reverse_foreign_key_create(self): - data = deepcopy(self.new_target_data) - data['sources'].append({'name': u'source-4', 'target': 2}) - instance = ForeignKeyTarget.objects.get(pk=2) - self.save_serialized_target(instance, data) - - # Ensure target 2 has new source and everything else is as expected - expected = deepcopy(self.data) - expected[1]['sources'].append({'id': 4, 'name': 'source-4', 'target': 2}) - self.check_serialized_targets(expected) - - def test_reverse_foreign_key_update(self): - data = deepcopy(self.target_data) - data['sources'][0]['name'] = 'source-1-changed' - data['sources'][2]['name'] = 'source-3-changed' - instance = ForeignKeyTarget.objects.get(pk=1) - self.save_serialized_target(instance, data) - - # Ensure target 1 is updated, and everything else is as expected - expected = deepcopy(self.data) - expected[0]['sources'][0]['name'] = 'source-1-changed' - expected[0]['sources'][2]['name'] = 'source-3-changed' - self.check_serialized_targets(expected) - - def test_reverse_foreign_key_delete(self): - data = deepcopy(self.target_data) - del data['sources'][2] - instance = ForeignKeyTarget.objects.get(pk=1) - self.save_serialized_target(instance, data) - - # Ensure target 1 has 2 sources and everything else is as expected - expected = deepcopy(self.data) - del expected[0]['sources'][2] - self.check_serialized_targets(expected) From c13f132a21f5a0902cef74dfea12e586561774ca Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Tue, 18 Dec 2012 14:09:58 +0200 Subject: [PATCH 082/129] Failing test with partial serializer and foreign keys --- rest_framework/tests/serializer.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 780177aa0..693c81c29 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -297,6 +297,38 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']}) + def test_foreign_key_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test foreign key validation. + """ + + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True) + self.assertTrue(photo_serializer.is_valid()) + self.assertTrue(photo_serializer.save()) + + # Updating only the description + photo_serializer = PhotoSerializer(instance=photo, + data={'description': 'new'}, + partial=True) + + self.assertTrue(photo_serializer.is_valid()) + self.assertTrue(photo_serializer.save()) + + class RegexValidationTest(TestCase): def test_create_failed(self): From 5c680c36e435ffa29ba6ed5f439d22ea4e849c67 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Wed, 19 Dec 2012 09:26:40 -0800 Subject: [PATCH 083/129] Check if RelatedField is required. --- rest_framework/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2060e3e70..b0dfdce8c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -351,6 +351,11 @@ class RelatedField(WritableField): if self.read_only: return + if field_name not in data and self.required: + raise ValidationError(self.error_messages['required']) + elif field_name not in data: + return + value = data.get(field_name) if value in (None, '') and not self.null: From d90d5380d730b992b1b59aaf9d1f89fbbbba0f9f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 22:05:00 +0000 Subject: [PATCH 084/129] pep8 --- rest_framework/tests/renderers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 9be4b1146..fb843676c 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -444,19 +444,19 @@ class CacheRenderTest(TestCase): return if state == None: return - if isinstance(state,tuple): - if not isinstance(state[0],dict): - state=state[1] + if isinstance(state, tuple): + if not isinstance(state[0], dict): + state = state[1] else: - state=state[0].update(state[1]) + state = state[0].update(state[1]) result = {} for i in state: try: - pickle.dumps(state[i],protocol=2) + pickle.dumps(state[i], protocol=2) except pickle.PicklingError: if not state[i] in seen: seen.append(state[i]) - result[i] = cls._get_pickling_errors(state[i],seen) + result[i] = cls._get_pickling_errors(state[i], seen) return result def http_resp(self, http_method, url): From 598ae3286ac6343a59e6d80fc93428539c5f836e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 22:05:35 +0000 Subject: [PATCH 085/129] Fix #521. (Browseable API exception on delete) --- rest_framework/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8dc0c3295..2700606d0 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -124,6 +124,6 @@ class DestroyModelMixin(object): Should be mixed in with `SingleObjectBaseView`. """ def destroy(self, request, *args, **kwargs): - self.object = self.get_object() - self.object.delete() + obj = self.get_object() + obj.delete() return Response(status=status.HTTP_204_NO_CONTENT) From c29b08ad43c8de5f295176eaf0270427f3a737f3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 22:06:38 +0000 Subject: [PATCH 086/129] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 87aaefbdb..c75f08790 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -8,6 +8,7 @@ ### Master +* Bugfix: Fix exception in browseable API on DELETE. * Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. ### 2.1.11 From 566b9ff27b93dfa089d054552a94d930e3f17c67 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 22:41:00 +0000 Subject: [PATCH 087/129] Added @joual - Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index c169fd74e..fa378cb28 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -79,6 +79,7 @@ The following people have helped make REST framework great. * Colin Murtaugh - [cmurtaugh] * Simon Pantzare - [pilt] * Szymon Teżewski - [sunscrapers] +* Joel Marcotte - [joual] Many thanks to everyone who's contributed to the project. @@ -193,3 +194,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [cmurtaugh]: https://github.com/cmurtaugh [pilt]: https://github.com/pilt [sunscrapers]: https://github.com/sunscrapers +[joual]: https://github.com/joual From c097bcef586da4513d1d6f357d9eb3d7b4b0fffb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 22:42:11 +0000 Subject: [PATCH 088/129] Update release notes. --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c75f08790..741d908cf 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -8,6 +8,7 @@ ### Master +* Bugfix: Fix bug that could occur using ChoiceField. * Bugfix: Fix exception in browseable API on DELETE. * Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. From a493c83248535c9fa7f78815b16bce7e88bf7966 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 23:12:27 +0000 Subject: [PATCH 089/129] urls, patterns, include imports move to compat to support incoming 1.3 thru 1.6 import compatability --- rest_framework/compat.py | 6 ++++++ rest_framework/tests/authentication.py | 10 ++++------ rest_framework/tests/breadcrumbs.py | 2 +- rest_framework/tests/decorators.py | 2 +- rest_framework/tests/htmlrenderer.py | 2 +- rest_framework/tests/hyperlink_relations.py | 2 +- rest_framework/tests/hyperlinkedserializers.py | 2 +- rest_framework/tests/models.py | 5 +++-- rest_framework/tests/renderers.py | 3 +-- rest_framework/tests/request.py | 5 ++--- rest_framework/tests/response.py | 5 +---- rest_framework/tests/reverse.py | 2 +- rest_framework/tests/serializer.py | 4 ++++ rest_framework/tests/testcases.py | 6 ++++-- rest_framework/urls.py | 2 +- 15 files changed, 32 insertions(+), 26 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d4901437d..86952fb8d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,6 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages. # flake8: noqa import django +# location of patterns, url, include changes in 1.4 onwards +try: + from django.conf.urls import patterns, url, include +except: + from django.conf.urls.defaults import patterns, url, include + # django-filter is optional try: import django_filters diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index d498ae3e0..838e081bb 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,15 +1,13 @@ -from django.conf.urls.defaults import patterns from django.contrib.auth.models import User -from django.test import Client, TestCase - -from django.utils import simplejson as json from django.http import HttpResponse +from django.test import Client, TestCase +from django.utils import simplejson as json -from rest_framework.views import APIView from rest_framework import permissions - from rest_framework.authtoken.models import Token from rest_framework.authentication import TokenAuthentication +from rest_framework.compat import patterns +from rest_framework.views import APIView import base64 diff --git a/rest_framework/tests/breadcrumbs.py b/rest_framework/tests/breadcrumbs.py index 647ab96d0..df8916832 100644 --- a/rest_framework/tests/breadcrumbs.py +++ b/rest_framework/tests/breadcrumbs.py @@ -1,5 +1,5 @@ -from django.conf.urls.defaults import patterns, url from django.test import TestCase +from rest_framework.compat import patterns, url from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.views import APIView diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 41864d71e..8079c8cb3 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,7 +1,7 @@ from django.test import TestCase +from django.test.client import RequestFactory from rest_framework import status from rest_framework.response import Response -from django.test.client import RequestFactory from rest_framework.renderers import JSONRenderer from rest_framework.parsers import JSONParser from rest_framework.authentication import BasicAuthentication diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 4caed59ee..54096206d 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -1,9 +1,9 @@ from django.core.exceptions import PermissionDenied -from django.conf.urls.defaults import patterns, url from django.http import Http404 from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader +from rest_framework.compat import patterns, url from rest_framework.decorators import api_view, renderer_classes from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response diff --git a/rest_framework/tests/hyperlink_relations.py b/rest_framework/tests/hyperlink_relations.py index 8f0238737..9e8ecf709 100644 --- a/rest_framework/tests/hyperlink_relations.py +++ b/rest_framework/tests/hyperlink_relations.py @@ -1,7 +1,7 @@ -from django.conf.urls import patterns, url from django.db import models from django.test import TestCase from rest_framework import serializers +from rest_framework.compat import patterns, url def dummy_view(request, pk): diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 24bf61bf8..ee4d8e577 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -1,8 +1,8 @@ -from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, status, serializers +from rest_framework.compat import patterns, url from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel factory = RequestFactory() diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 1b1def30d..0759650ab 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,9 +52,10 @@ class RESTFrameworkModel(models.Model): class HasPositiveIntegerAsChoice(RESTFrameworkModel): - some_choices = ((1,'A'),(2,'B'),(3,'C')) + some_choices = ((1, 'A'), (2, 'B'), (3, 'C')) some_integer = models.PositiveIntegerField(choices=some_choices) + class Anchor(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') @@ -164,7 +165,7 @@ class Photo(RESTFrameworkModel): # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=True) + title = models.CharField(max_length=100, blank=True, null=False) # Model for issue #380 diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index fb843676c..c1b4e624b 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,13 +1,12 @@ import pickle import re -from django.conf.urls.defaults import patterns, url, include from django.core.cache import cache from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status, permissions -from rest_framework.compat import yaml +from rest_framework.compat import yaml, patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 2850992d2..2eb34ceaa 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -1,16 +1,15 @@ """ Tests for content parsing, and form-overloaded content parsing. """ -from django.conf.urls.defaults import patterns from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware from django.test import TestCase, Client +from django.test.client import RequestFactory from django.utils import simplejson as json - from rest_framework import status from rest_framework.authentication import SessionAuthentication -from django.test.client import RequestFactory +from rest_framework.compat import patterns from rest_framework.parsers import ( BaseParser, FormParser, diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index d7b75450c..875f4d422 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,8 +1,5 @@ -import unittest - -from django.conf.urls.defaults import patterns, url, include from django.test import TestCase - +from rest_framework.compat import patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py index fd9a7d64e..8c86e1fbe 100644 --- a/rest_framework/tests/reverse.py +++ b/rest_framework/tests/reverse.py @@ -1,6 +1,6 @@ -from django.conf.urls.defaults import patterns, url from django.test import TestCase from django.test.client import RequestFactory +from rest_framework.compat import patterns, url from rest_framework.reverse import reverse factory = RequestFactory() diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 7f2c27b05..6ea4b4246 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -699,6 +699,10 @@ class BlankFieldTests(TestCase): serializer = self.model_serializer_class(data=self.data) self.assertEquals(serializer.is_valid(), True) + def test_create_model_null_field(self): + serializer = self.model_serializer_class(data={'title': None}) + self.assertEquals(serializer.is_valid(), True) + def test_create_not_blank_field(self): """ Test to ensure blank data in a field not marked as blank=True diff --git a/rest_framework/tests/testcases.py b/rest_framework/tests/testcases.py index c90224aa6..97f492ff4 100644 --- a/rest_framework/tests/testcases.py +++ b/rest_framework/tests/testcases.py @@ -6,6 +6,7 @@ from django.test import TestCase NO_SETTING = ('!', None) + class TestSettingsManager(object): """ A class which can modify some Django settings temporarily for a @@ -19,7 +20,7 @@ class TestSettingsManager(object): self._original_settings = {} def set(self, **kwargs): - for k,v in kwargs.iteritems(): + for k, v in kwargs.iteritems(): self._original_settings.setdefault(k, getattr(settings, k, NO_SETTING)) setattr(settings, k, v) @@ -31,7 +32,7 @@ class TestSettingsManager(object): call_command('syncdb', verbosity=0) def revert(self): - for k,v in self._original_settings.iteritems(): + for k, v in self._original_settings.iteritems(): if v == NO_SETTING: delattr(settings, k) else: @@ -57,6 +58,7 @@ class SettingsTestCase(TestCase): def tearDown(self): self.settings_manager.revert() + class TestModelsTestCase(SettingsTestCase): def setUp(self, *args, **kwargs): installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',) diff --git a/rest_framework/urls.py b/rest_framework/urls.py index bcdc23e74..fbe4bc07e 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`. url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) ) """ -from django.conf.urls.defaults import patterns, url +from rest_framework.compat import patterns, url template_name = {'template_name': 'rest_framework/login.html'} From c27295dcbdf89b3cbc10d6325833e428c66c0f2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 19 Dec 2012 23:12:57 +0000 Subject: [PATCH 090/129] Update minor Django versions in tox --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 69eb38237..22c85e493 100644 --- a/tox.ini +++ b/tox.ini @@ -12,12 +12,12 @@ deps = https://github.com/django/django/zipball/master [testenv:py2.7-django1.4] basepython = python2.7 -deps = django==1.4.1 +deps = django==1.4.3 django-filter==0.5.4 [testenv:py2.7-django1.3] basepython = python2.7 -deps = django==1.3.3 +deps = django==1.3.5 django-filter==0.5.4 [testenv:py2.6-django1.5] @@ -27,10 +27,10 @@ deps = https://github.com/django/django/zipball/master [testenv:py2.6-django1.4] basepython = python2.6 -deps = django==1.4.1 +deps = django==1.4.3 django-filter==0.5.4 [testenv:py2.6-django1.3] basepython = python2.6 -deps = django==1.3.3 +deps = django==1.3.5 django-filter==0.5.4 From 125f027d2d124e2a483406d9d70e447b8634350c Mon Sep 17 00:00:00 2001 From: Mark Hughes Date: Thu, 20 Dec 2012 23:48:10 +0000 Subject: [PATCH 091/129] Added setter to the auth property --- rest_framework/request.py | 8 ++++++++ rest_framework/tests/request.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/rest_framework/request.py b/rest_framework/request.py index 39c643219..b7133608a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -188,6 +188,14 @@ class Request(object): self._user, self._auth = self._authenticate() return self._auth + @auth.setter + def auth(self, value): + """ + Sets any non-user authentication information associated with the + request, such as an authentication token. + """ + self._auth = value + def _load_data_and_files(self): """ Parses the request content into self.DATA and self.FILES. diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 2eb34ceaa..1f05ff8ff 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -303,3 +303,11 @@ class TestUserSetter(TestCase): self.assertFalse(self.request.user.is_anonymous()) logout(self.request) self.assertTrue(self.request.user.is_anonymous()) + + +class TestAuthSetter(TestCase): + + def test_auth_can_be_set(self): + request = Request(factory.get('/')) + request.auth = 'DUMMY' + self.assertEqual(request.auth, 'DUMMY') From 79dc321f391cc44299acbfc4c9a00a82806c47d6 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Fri, 21 Dec 2012 09:58:05 +0100 Subject: [PATCH 092/129] Added virtualenv-generated directories to the .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 25113d09d..2255cd9aa 100644 --- a/.gitignore +++ b/.gitignore @@ -10,5 +10,10 @@ dist/ *.egg-info/ MANIFEST +bin/ +include/ +lib/ +local/ + !.gitignore !.travis.yml From 83feda6a69317025a0574af630faeaf254b99aed Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Dec 2012 09:10:16 +0000 Subject: [PATCH 093/129] Rename relationship tests so they show up together --- .../tests/{hyperlink_relations.py => relations_hyperlink.py} | 0 .../tests/{nested_relations.py => relations_nested.py} | 1 - rest_framework/tests/{pk_relations.py => relations_pk.py} | 0 3 files changed, 1 deletion(-) rename rest_framework/tests/{hyperlink_relations.py => relations_hyperlink.py} (100%) rename rest_framework/tests/{nested_relations.py => relations_nested.py} (98%) rename rest_framework/tests/{pk_relations.py => relations_pk.py} (100%) diff --git a/rest_framework/tests/hyperlink_relations.py b/rest_framework/tests/relations_hyperlink.py similarity index 100% rename from rest_framework/tests/hyperlink_relations.py rename to rest_framework/tests/relations_hyperlink.py diff --git a/rest_framework/tests/nested_relations.py b/rest_framework/tests/relations_nested.py similarity index 98% rename from rest_framework/tests/nested_relations.py rename to rest_framework/tests/relations_nested.py index b9022cac2..0d18d4adc 100644 --- a/rest_framework/tests/nested_relations.py +++ b/rest_framework/tests/relations_nested.py @@ -1,4 +1,3 @@ -from copy import deepcopy from django.db import models from django.test import TestCase from rest_framework import serializers diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/relations_pk.py similarity index 100% rename from rest_framework/tests/pk_relations.py rename to rest_framework/tests/relations_pk.py From 37acea52406edcb1c14f52360c8262ad992b8e4a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Dec 2012 09:15:08 +0000 Subject: [PATCH 094/129] Fix up test url model now filename has changed --- rest_framework/tests/relations_hyperlink.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 9e8ecf709..53ce00744 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -78,7 +78,7 @@ class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer) # TODO: Add test that .data cannot be accessed prior to .is_valid class HyperlinkedManyToManyTests(TestCase): - urls = 'rest_framework.tests.hyperlink_relations' + urls = 'rest_framework.tests.relations_hyperlink' def setUp(self): for idx in range(1, 4): @@ -186,7 +186,7 @@ class HyperlinkedManyToManyTests(TestCase): class HyperlinkedForeignKeyTests(TestCase): - urls = 'rest_framework.tests.hyperlink_relations' + urls = 'rest_framework.tests.relations_hyperlink' def setUp(self): target = ForeignKeyTarget(name='target-1') @@ -243,7 +243,7 @@ class HyperlinkedForeignKeyTests(TestCase): class HyperlinkedNullableForeignKeyTests(TestCase): - urls = 'rest_framework.tests.hyperlink_relations' + urls = 'rest_framework.tests.relations_hyperlink' def setUp(self): target = ForeignKeyTarget(name='target-1') From 04119245ec5dc80f7376e319e5876b9fb6711c45 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Dec 2012 09:15:28 +0000 Subject: [PATCH 095/129] Make nested tests consistent with pk and hyperlink tests --- rest_framework/tests/relations_nested.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 0d18d4adc..3482c2522 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -35,18 +35,17 @@ class ReverseForeignKeyTests(TestCase): for idx in range(1, 4): source = ForeignKeySource(name='source-%d' % idx, target=target) source.save() - self.target_data = {'id': 1, 'name': u'target-1', 'sources': [ + + def test_reverse_foreign_key_retrieve(self): + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': [ {'id': 1, 'name': u'source-1', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1}, {'id': 3, 'name': u'source-3', 'target': 1}, + ]}, + {'id': 2, 'name': u'target-2', 'sources': [ ]} - self.new_target_data = {'id': 2, 'name': u'target-2', 'sources': []} - self.data = [self.target_data, self.new_target_data] - - def check_serialized_targets(self, data): - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset) - self.assertEquals(serializer.data, data) - - def test_reverse_foreign_key_retrieve(self): - self.check_serialized_targets(self.data) + ] + self.assertEquals(serializer.data, expected) From fb4bbf7d985be21511b412011fb949182bc1b73e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Dec 2012 09:17:33 +0000 Subject: [PATCH 096/129] Added @treyhunner. Thank you! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index fa378cb28..8b8cac1a9 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -80,6 +80,7 @@ The following people have helped make REST framework great. * Simon Pantzare - [pilt] * Szymon Teżewski - [sunscrapers] * Joel Marcotte - [joual] +* Trey Hunner - [treyhunner] Many thanks to everyone who's contributed to the project. @@ -195,3 +196,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [pilt]: https://github.com/pilt [sunscrapers]: https://github.com/sunscrapers [joual]: https://github.com/joual +[treyhunner]: https://github.com/treyhunner From 79aea2f0d082f17e7bb75cc32bd71b5f04836d43 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Dec 2012 09:18:35 +0000 Subject: [PATCH 097/129] Version 2.1.12 --- README.md | 8 ++++++++ docs/topics/release-notes.md | 4 +++- rest_framework/__init__.py | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4078b15c..13a78724f 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,14 @@ To run the tests. # Changelog +### 2.1.12 + +**Date**: 21st Dec 2012 + +* Bugfix: Fix bug that could occur using ChoiceField. +* Bugfix: Fix exception in browseable API on DELETE. +* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. + ## 2.1.11 **Date**: 17th Dec 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 741d908cf..6dedc3d22 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,7 +6,9 @@ ## 2.1.x series -### Master +### 2.1.12 + +**Date**: 21st Dec 2012 * Bugfix: Fix bug that could occur using ChoiceField. * Bugfix: Fix exception in browseable API on DELETE. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index d5cac5c6d..02bc6fc1d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.11' +__version__ = '2.1.12' VERSION = __version__ # synonym From 5f9ecd1c7ad2f52ad5711d2a89bb1884f5b662f9 Mon Sep 17 00:00:00 2001 From: Reinout van Rees Date: Fri, 21 Dec 2012 10:42:40 +0100 Subject: [PATCH 098/129] slug_kwarg attribute doesn't work; it should be slug_url_kwarg --- docs/api-guide/generic-views.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 428323b89..27c7d3f60 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -7,11 +7,11 @@ > > — [Django Documentation][cite] -One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns. +One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns. The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. -If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views. +If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views. ## Examples @@ -29,7 +29,7 @@ For more complex cases you might also want to override various methods on the vi model = User serializer_class = UserSerializer permission_classes = (IsAdminUser,) - + def get_paginate_by(self, queryset): """ Use smaller pagination for HTML representations. @@ -150,14 +150,14 @@ Provides a base view for acting on a single object, by combining REST framework' * `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`. * `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+] -* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+] +* `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+] * `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`. --- # Mixins -The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour. +The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour. ## ListModelMixin @@ -220,4 +220,4 @@ Should be mixed in with [SingleObjectAPIView]. [CreateModelMixin]: #createmodelmixin [RetrieveModelMixin]: #retrievemodelmixin [UpdateModelMixin]: #updatemodelmixin -[DestroyModelMixin]: #destroymodelmixin \ No newline at end of file +[DestroyModelMixin]: #destroymodelmixin From f8a1256b1c006ee9ab46645a11ef19123b429a56 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 21 Dec 2012 11:33:01 -0800 Subject: [PATCH 099/129] Update RelatedField#field_from_native coding style --- rest_framework/fields.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1059df918..dd90c3f86 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -351,13 +351,13 @@ class RelatedField(WritableField): if self.read_only: return - if field_name not in data and self.required: - raise ValidationError(self.error_messages['required']) - elif field_name not in data: + try: + value = data[field_name] + except KeyError: + if self.required: + raise ValidationError(self.error_messages['required']) return - value = data.get(field_name) - if value in (None, '') and not self.null: raise ValidationError('Value may not be null') elif value in (None, '') and self.null: From ed09d26fb8da2391cab4b096d276028384438eb6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 23 Dec 2012 18:50:17 +0000 Subject: [PATCH 100/129] Update docs/topics/release-notes.md --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 6dedc3d22..71fa3c03d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -6,6 +6,10 @@ ## 2.1.x series +### Master + +* Bugfix: Related fields now respect the required flag, and may be required=False. + ### 2.1.12 **Date**: 21st Dec 2012 From 5ae7786930ea0e6ec35f81ca7a38f36a80a4c6b7 Mon Sep 17 00:00:00 2001 From: Roman Akinfold Date: Thu, 27 Dec 2012 05:35:03 +0800 Subject: [PATCH 101/129] Replace get_static_prefix templatetag with static so now static files urls in html view are correct if we use cloud storage to serve static files --- .../templates/rest_framework/base.html | 18 +++++++++--------- .../templates/rest_framework/login.html | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index fb0e19f05..0e09e5068 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,6 +1,6 @@ {% load url from future %} {% load rest_framework %} -{% load static %} +{% load staticfiles %} @@ -14,10 +14,10 @@ {% block title %}Django REST framework{% endblock %} {% block style %} - - - - + + + + {% endblock %} {% endblock %} @@ -195,10 +195,10 @@ {% endblock %} {% block script %} - - - - + + + + {% endblock %} diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index c1271399b..3681a1583 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -1,11 +1,11 @@ {% load url from future %} -{% load static %} +{% load staticfiles %} - - - + + + From 24ed6dcfdadb5c1e7b18a1b1dfabad871ee91f09 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Dec 2012 17:35:00 +0000 Subject: [PATCH 102/129] Update runtest settings to include staticfiles app. --- rest_framework/runtests/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index dd5d9dc3c..90fbaa98c 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -91,6 +91,7 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', + 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: From 9cc56a97b55e183d8131665d1409649379a969f0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Dec 2012 17:35:21 +0000 Subject: [PATCH 103/129] Added @akinfold for staticfiles changes. --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 8b8cac1a9..e4b8da4c8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -81,6 +81,7 @@ The following people have helped make REST framework great. * Szymon Teżewski - [sunscrapers] * Joel Marcotte - [joual] * Trey Hunner - [treyhunner] +* Roman Akinfold - [akinfold] Many thanks to everyone who's contributed to the project. @@ -197,3 +198,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [sunscrapers]: https://github.com/sunscrapers [joual]: https://github.com/joual [treyhunner]: https://github.com/treyhunner +[akinfold]: https://github.com/akinfold From 4b32cda5a8a32bf43e80dfbe6d9d17a07b20152c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Dec 2012 17:53:07 +0000 Subject: [PATCH 104/129] Update release notes and documentation for 2.2.0 --- README.md | 34 ++++++++++++++++++++++++++++++- docs/index.md | 19 ++++++++++++++---- docs/topics/release-notes.md | 39 ++++++++++++++++++++++++++---------- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 13a78724f..7308c577e 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,46 @@ There is also a sandbox API you can use for testing purposes, [available here][s # Installation -Install using `pip`... +Install using `pip`, including any optional packages you want... pip install djangorestframework + pip install markdown # Markdown support for the browseable API. + pip install pyyaml # YAML content-type support. + pip install django-filter # Filtering support ...or clone the project from github. git clone git@github.com:tomchristie/django-rest-framework.git + cd django-rest-framework pip install -r requirements.txt + pip install -r optionals.txt + +Add `'rest_framework'` to your `INSTALLED_APPS` setting. + + INSTALLED_APPS = ( + ... + 'rest_framework', + ) + +## Browseable API requirements + +If you're intending to use the browseable API you'll also want to also ensure you include `'django.contrib.staticfiles'` in your `INSTALLED_APPS` setting. + + INSTALLED_APPS = ( + ... + 'django.contrib.staticfiles', + 'rest_framework', + ) + +You'll proabably also want to add REST framework's login and logout views. +Add the following to your root `urls.py` file. + + urlpatterns = patterns('', + ... + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) + ) + +Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace. # Development diff --git a/docs/index.md b/docs/index.md index cc0f2a139..5d90e9e5d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ Django REST framework is a lightweight library that makes it easy to build Web A Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. -If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. +If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. There is also a sandbox API you can use for testing purposes, [available here][sandbox]. @@ -52,21 +52,32 @@ Install using `pip`, including any optional packages you want... pip install -r requirements.txt pip install -r optionals.txt -Add `rest_framework` to your `INSTALLED_APPS`. +Add `'rest_framework'` to your `INSTALLED_APPS` setting. INSTALLED_APPS = ( ... 'rest_framework', ) -If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file. +### Browseable API requirements + +If you're intending to use the browseable API you'll also want to also ensure you include `'django.contrib.staticfiles'` in your `INSTALLED_APPS` setting. + + INSTALLED_APPS = ( + ... + 'django.contrib.staticfiles', + 'rest_framework', + ) + +You'll proabably also want to add REST framework's login and logout views. +Add the following to your root `urls.py` file. urlpatterns = patterns('', ... url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) ) -Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace. +Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace. ## Quickstart diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 71fa3c03d..1934292c8 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,12 +4,27 @@ > > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -## 2.1.x series +## Versioning -### Master +Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. +Medium version numbers (0.x.0) may include minor API changes. You should read the release notes carefully before upgrading between medium point releases. + +## 2.2.x series + +### 2.2.0 + +**Date**: 27th Dec 2012 + +* Support configurable `STATICFILES_STORAGE` storage. * Bugfix: Related fields now respect the required flag, and may be required=False. +**API-incompatible changes**: From 2.2.0 Onwards you must make sure to include `'django.contrib.staticfiles'` in your `INSTALLED_APPS`. This is in line with Django's 1.4's recommended usage of [the `'staticfiles'` template tag][staticfiles14] instead of Django 1.3's recommended usage of [the `'static'` template tag][staticfiles13]. + +--- + +## 2.1.x series + ### 2.1.12 **Date**: 21st Dec 2012 @@ -105,7 +120,7 @@ * Support use of HTML exception templates. Eg. `403.html` * Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments. -* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs. +* Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs. * Bugfix: Make textareas same width as other fields in browsable API. * Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. @@ -113,8 +128,6 @@ **Date**: 5th Nov 2012 -**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. - * **Serializer `instance` and `data` keyword args have their position swapped.** * `queryset` argument is now optional on writable model fields. * Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments. @@ -123,6 +136,8 @@ * Bugfix: Support choice field in Browseable API. * Bugfix: Related fields with `read_only=True` do not require a `queryset` argument. +**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. + --- ## 2.0.x series @@ -159,9 +174,9 @@ * Allow views to specify template used by TemplateRenderer * More consistent error responses * Some serializer fixes -* Fix internet explorer ajax behaviour +* Fix internet explorer ajax behavior * Minor xml and yaml fixes -* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) +* Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) * Sensible absolute URL generation, not using hacky set_script_prefix --- @@ -172,13 +187,13 @@ * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions. * Use `staticfiles` for css files. - - Easier to override. Won't conflict with customised admin styles (eg grappelli) + - Easier to override. Won't conflict with customized admin styles (e.g. grappelli) * Templates are now nicely namespaced. - Allows easier overriding. * Drop implied 'pk' filter if last arg in urlconf is unnamed. - - Too magical. Explict is better than implicit. -* Saner template variable autoescaping. -* Tider setup.py + - Too magical. Explicit is better than implicit. +* Saner template variable auto-escaping. +* Tidier setup.py * Updated for URLObject 2.0 * Bugfixes: - Bug with PerUserThrottling when user contains unicode chars. @@ -266,5 +281,7 @@ * Initial release. [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html +[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag +[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [announcement]: rest-framework-2-announcement.md From eefd5b05fbd7639fa813728bde44f1614875866e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Dec 2012 20:20:01 +0000 Subject: [PATCH 105/129] Use compat import of urlpatterns --- rest_framework/runtests/urls.py | 2 +- rest_framework/tests/modelviews.py | 2 +- rest_framework/urlpatterns.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py index 4b7da7871..ed5baeae6 100644 --- a/rest_framework/runtests/urls.py +++ b/rest_framework/runtests/urls.py @@ -1,7 +1,7 @@ """ Blank URLConf just to keep runtests.py happy. """ -from django.conf.urls.defaults import * +from rest_framework.compat import patterns urlpatterns = patterns('', ) diff --git a/rest_framework/tests/modelviews.py b/rest_framework/tests/modelviews.py index 1f8468e84..f12e3b979 100644 --- a/rest_framework/tests/modelviews.py +++ b/rest_framework/tests/modelviews.py @@ -1,4 +1,4 @@ -# from django.conf.urls.defaults import patterns, url +# from rest_framework.compat import patterns, url # from django.forms import ModelForm # from django.contrib.auth.models import Group, User # from rest_framework.resources import ModelResource diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 0ad926fa1..143928c99 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import url +from rest_framework.compat import url from rest_framework.settings import api_settings From 3e4242fc43def87fc6e0bda11afd8d3a82bd3ae3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Dec 2012 12:33:24 +0000 Subject: [PATCH 106/129] django.contrib.staticfiles no longer needs to be in INSTALLED_APPS --- README.md | 13 +------------ docs/index.md | 13 +------------ docs/topics/release-notes.md | 18 ++++++++---------- rest_framework/runtests/settings.py | 1 - 4 files changed, 10 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 7308c577e..87e524a75 100644 --- a/README.md +++ b/README.md @@ -60,18 +60,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting. 'rest_framework', ) -## Browseable API requirements - -If you're intending to use the browseable API you'll also want to also ensure you include `'django.contrib.staticfiles'` in your `INSTALLED_APPS` setting. - - INSTALLED_APPS = ( - ... - 'django.contrib.staticfiles', - 'rest_framework', - ) - -You'll proabably also want to add REST framework's login and logout views. -Add the following to your root `urls.py` file. +If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file. urlpatterns = patterns('', ... diff --git a/docs/index.md b/docs/index.md index 5d90e9e5d..69d972d0e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,18 +59,7 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting. 'rest_framework', ) -### Browseable API requirements - -If you're intending to use the browseable API you'll also want to also ensure you include `'django.contrib.staticfiles'` in your `INSTALLED_APPS` setting. - - INSTALLED_APPS = ( - ... - 'django.contrib.staticfiles', - 'rest_framework', - ) - -You'll proabably also want to add REST framework's login and logout views. -Add the following to your root `urls.py` file. +If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file. urlpatterns = patterns('', ... diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 1934292c8..0a17f1a6a 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -10,21 +10,19 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You Medium version numbers (0.x.0) may include minor API changes. You should read the release notes carefully before upgrading between medium point releases. -## 2.2.x series - -### 2.2.0 - -**Date**: 27th Dec 2012 - -* Support configurable `STATICFILES_STORAGE` storage. -* Bugfix: Related fields now respect the required flag, and may be required=False. - -**API-incompatible changes**: From 2.2.0 Onwards you must make sure to include `'django.contrib.staticfiles'` in your `INSTALLED_APPS`. This is in line with Django's 1.4's recommended usage of [the `'staticfiles'` template tag][staticfiles14] instead of Django 1.3's recommended usage of [the `'static'` template tag][staticfiles13]. +Major version numbers (x.0.0) are reserved for project milestones. No major point releases are currently planned. --- ## 2.1.x series +### 2.1.13 + +**Date**: 28th Dec 2012 + +* Support configurable `STATICFILES_STORAGE` storage. +* Bugfix: Related fields now respect the required flag, and may be required=False. + ### 2.1.12 **Date**: 21st Dec 2012 diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 90fbaa98c..dd5d9dc3c 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -91,7 +91,6 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', - 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: From acdb69be527f48b245a0e8cd14a3628a3c75347a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Dec 2012 12:36:28 +0000 Subject: [PATCH 107/129] Include 'static' template tag to enable 1.3 compatible staticfiles behaviour --- .../templates/rest_framework/base.html | 1 - .../templates/rest_framework/login.html | 1 - rest_framework/templatetags/rest_framework.py | 83 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 0e09e5068..42e49cb90 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,6 +1,5 @@ {% load url from future %} {% load rest_framework %} -{% load staticfiles %} diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index 3681a1583..e3f3d799b 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -1,5 +1,4 @@ {% load url from future %} -{% load staticfiles %} diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 4e0181ee0..09c658bcd 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -11,6 +11,89 @@ import string register = template.Library() +# Note we don't use 'load staticfiles', because we need a 1.3 compatible +# version, so instead we include the `static` template tag ourselves. + +# When 1.3 becomes unsupported by REST framework, we can instead start to +# use the {% load staticfiles %} tag, remove the following code, +# and add a dependancy that `django.contrib.staticfiles` must be installed. + +# Note: We can't put this into the `compat` module because the compat import +# from rest_framework.compat import ... +# conflicts with this rest_framework template tag module. + +try: # Django 1.5+ + from django.contrib.staticfiles.templatetags import StaticFilesNode + + @register.tag('static') + def do_static(parser, token): + return StaticFilesNode.handle_token(parser, token) + +except: + try: # Django 1.4 + from django.contrib.staticfiles.storage import staticfiles_storage + + @register.simple_tag + def static(path): + """ + A template tag that returns the URL to a file + using staticfiles' storage backend + """ + return staticfiles_storage.url(path) + + except: # Django 1.3 + from urlparse import urljoin + from django import template + from django.templatetags.static import PrefixNode + + class StaticNode(template.Node): + def __init__(self, varname=None, path=None): + if path is None: + raise template.TemplateSyntaxError( + "Static template nodes must be given a path to return.") + self.path = path + self.varname = varname + + def url(self, context): + path = self.path.resolve(context) + return self.handle_simple(path) + + def render(self, context): + url = self.url(context) + if self.varname is None: + return url + context[self.varname] = url + return '' + + @classmethod + def handle_simple(cls, path): + return urljoin(PrefixNode.handle_simple("STATIC_URL"), path) + + @classmethod + def handle_token(cls, parser, token): + """ + Class method to parse prefix node and return a Node. + """ + bits = token.split_contents() + + if len(bits) < 2: + raise template.TemplateSyntaxError( + "'%s' takes at least one argument (path to file)" % bits[0]) + + path = parser.compile_filter(bits[1]) + + if len(bits) >= 2 and bits[-2] == 'as': + varname = bits[3] + else: + varname = None + + return cls(varname, path) + + @register.tag('static') + def do_static_13(parser, token): + return StaticNode.handle_token(parser, token) + + def replace_query_param(url, key, val): """ Given a URL and a key/val pair, set or replace an item in the query From f7a82b6aeebaff2df78366a1e0b087c5afb2f459 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Dec 2012 12:45:05 +0000 Subject: [PATCH 108/129] Version 2.1.13 --- README.md | 7 +++++++ rest_framework/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 87e524a75..c262cd684 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,13 @@ To run the tests. # Changelog +### 2.1.13 + +**Date**: 28th Dec 2012 + +* Support configurable `STATICFILES_STORAGE` storage. +* Bugfix: Related fields now respect the required flag, and may be required=False. + ### 2.1.12 **Date**: 21st Dec 2012 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 02bc6fc1d..2e38d8639 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.12' +__version__ = '2.1.13' VERSION = __version__ # synonym From 5d4ea3d23fdb173b4109a64b2d4231d93d394387 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Dec 2012 12:59:24 +0000 Subject: [PATCH 109/129] Add .validate() example --- docs/api-guide/serializers.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 19efde3c7..da1efb8ff 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -110,7 +110,22 @@ Your `validate_` methods should either just return the `attrs` dictio ### Object-level validation -To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. +To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example: + + from rest_framework import serializers + + class EventSerializer(serializers.Serializer): + description = serializers.CahrField(max_length=100) + start = serializers.DateTimeField() + finish = serializers.DateTimeField() + + def validate(self, attrs): + """ + Check that the start is before the stop. + """ + if attrs['start'] < attrs['finish']: + raise serializers.ValidationError("finish must occur after start") + return attrs ## Saving object state From 1f6af163fece28db3ba7943edce2415a23874d44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 12:15:15 +0000 Subject: [PATCH 110/129] Tweak quote --- docs/api-guide/serializers.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index da1efb8ff..d98a602f7 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -4,8 +4,7 @@ > Expanding the usefulness of the serializers is something that we would like to address. However, it's not a trivial problem, and it -will take some serious design work. Any offers to help out in this -area would be gratefully accepted. +will take some serious design work. > > — Russell Keith-Magee, [Django users group][cite] From 250a7231d3b5e44bc5ff984ef9fd515bfd238a25 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 13:10:43 +0000 Subject: [PATCH 111/129] Add tests for nested FKs --- rest_framework/tests/relations_nested.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 3482c2522..297538c4a 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -15,12 +15,18 @@ class ForeignKeySource(models.Model): class ForeignKeySourceSerializer(serializers.ModelSerializer): + class Meta: + depth = 1 + model = ForeignKeySource + + +class FlatForeignKeySourceSerializer(serializers.ModelSerializer): class Meta: model = ForeignKeySource class ForeignKeyTargetSerializer(serializers.ModelSerializer): - sources = ForeignKeySourceSerializer() + sources = FlatForeignKeySourceSerializer() class Meta: model = ForeignKeyTarget @@ -36,6 +42,16 @@ class ReverseForeignKeyTests(TestCase): source = ForeignKeySource(name='source-%d' % idx, target=target) source.save() + def test_foreign_key_retrieve(self): + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, + {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, + {'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'target-1'}}, + ] + self.assertEquals(serializer.data, expected) + def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) From 923f81d26971510c12cb08e3061c7b37c0c6ffe8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 13:19:05 +0000 Subject: [PATCH 112/129] Nested serializers now support nullable relationships, plus test. Fixes #384 --- docs/topics/release-notes.md | 4 +++ rest_framework/serializers.py | 3 ++ rest_framework/tests/relations_nested.py | 35 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0a17f1a6a..dd54a613d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -16,6 +16,10 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ## 2.1.x series +### Master + +* Bugfix: Nested serializers now support nullable relationships. + ### 2.1.13 **Date**: 28th Dec 2012 diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8156bc185..d8350f95a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -307,6 +307,9 @@ class BaseSerializer(Field): if is_simple_callable(getattr(obj, 'all', None)): return [self.to_native(item) for item in obj.all()] + if obj is None: + return None + return self.to_native(obj) @property diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 297538c4a..b11473780 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -32,6 +32,20 @@ class ForeignKeyTargetSerializer(serializers.ModelSerializer): model = ForeignKeyTarget +# Nullable ForeignKey + +class NullableForeignKeySource(models.Model): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, + related_name='nullable_sources') + + +class NullableForeignKeySourceSerializer(serializers.ModelSerializer): + class Meta: + depth = 1 + model = NullableForeignKeySource + + class ReverseForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') @@ -65,3 +79,24 @@ class ReverseForeignKeyTests(TestCase): ]} ] self.assertEquals(serializer.data, expected) + + +class NestedNullableForeignKeyTests(TestCase): + def setUp(self): + target = ForeignKeyTarget(name='target-1') + target.save() + for idx in range(1, 4): + if idx == 3: + target = None + source = NullableForeignKeySource(name='source-%d' % idx, target=target) + source.save() + + def test_foreign_key_retrieve_with_null(self): + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, + {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, + {'id': 3, 'name': u'source-3', 'target': None}, + ] + self.assertEquals(serializer.data, expected) From bf69205cd33fc1601e5ae3c0c48ffcff1a62470b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 13:32:49 +0000 Subject: [PATCH 113/129] Tests for retrieving nullable relations --- rest_framework/tests/relations_hyperlink.py | 20 ++++++++++++++++---- rest_framework/tests/relations_pk.py | 20 ++++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 53ce00744..86d7b1c23 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -249,9 +249,21 @@ class HyperlinkedNullableForeignKeyTests(TestCase): target = ForeignKeyTarget(name='target-1') target.save() for idx in range(1, 4): + if idx == 3: + target = None source = NullableForeignKeySource(name='source-%d' % idx, target=target) source.save() + def test_foreign_key_retrieve_with_null(self): + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, + ] + self.assertEquals(serializer.data, expected) + def test_foreign_key_create_with_valid_null(self): data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) @@ -266,7 +278,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): expected = [ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -290,7 +302,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): expected = [ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -309,7 +321,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): expected = [ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) @@ -332,7 +344,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase): expected = [ {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, - {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index e3360939f..7a1fe07ca 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -229,9 +229,21 @@ class PKNullableForeignKeyTests(TestCase): target = ForeignKeyTarget(name='target-1') target.save() for idx in range(1, 4): + if idx == 3: + target = None source = NullableForeignKeySource(name='source-%d' % idx, target=target) source.save() + def test_foreign_key_retrieve_with_null(self): + queryset = NullableForeignKeySource.objects.all() + serializer = NullableForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 1}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': None}, + ] + self.assertEquals(serializer.data, expected) + def test_foreign_key_create_with_valid_null(self): data = {'id': 4, 'name': u'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) @@ -246,7 +258,7 @@ class PKNullableForeignKeyTests(TestCase): expected = [ {'id': 1, 'name': u'source-1', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': None}, {'id': 4, 'name': u'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -270,7 +282,7 @@ class PKNullableForeignKeyTests(TestCase): expected = [ {'id': 1, 'name': u'source-1', 'target': 1}, {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': None}, {'id': 4, 'name': u'source-4', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -289,7 +301,7 @@ class PKNullableForeignKeyTests(TestCase): expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 3, 'name': u'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) @@ -312,7 +324,7 @@ class PKNullableForeignKeyTests(TestCase): expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 1}, - {'id': 3, 'name': u'source-3', 'target': 1} + {'id': 3, 'name': u'source-3', 'target': None} ] self.assertEquals(serializer.data, expected) From 25398e8cf1b3c6e5f959c2f8063c9bcfc3b377c7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 13:32:56 +0000 Subject: [PATCH 114/129] Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index dd54a613d..f5d0fa077 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -18,6 +18,7 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ### Master +* Bugfix: Model fields with `blank=True` are now `required=False` by default. * Bugfix: Nested serializers now support nullable relationships. ### 2.1.13 From 51e2664491577cc218dfa0643e0c20532b3eb96e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 15:51:32 +0000 Subject: [PATCH 115/129] Add FK create tests --- rest_framework/tests/relations_pk.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 7a1fe07ca..e5391f1b4 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -216,6 +216,44 @@ class PKForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_create(self): + data = {'id': 4, 'name': u'source-4', 'target': 2} + serializer = ForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 1 is updated, and everything else is as expected + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 1}, + {'id': 2, 'name': u'source-2', 'target': 1}, + {'id': 3, 'name': u'source-3', 'target': 1}, + {'id': 4, 'name': u'source-4', 'target': 2}, + ] + self.assertEquals(serializer.data, expected) + + # TODO: See #511 + # def test_reverse_foreign_key_create(self): + # data = {'id': 3, 'name': u'target-3', 'sources': [1, 3]} + # serializer = ForeignKeyTargetSerializer(data=data) + # self.assertTrue(serializer.is_valid()) + # obj = serializer.save() + # self.assertEquals(serializer.data, data) + # self.assertEqual(obj.name, u'target-3') + + # # Ensure target 4 is added, and everything else is as expected + # queryset = ForeignKeyTarget.objects.all() + # serializer = ForeignKeyTargetSerializer(queryset) + # expected = [ + # {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, + # {'id': 2, 'name': u'target-2', 'sources': []}, + # {'id': 3, 'name': u'target-3', 'sources': [1, 3]}, + # ] + # self.assertEquals(serializer.data, expected) + def test_foreign_key_update_with_invalid_null(self): data = {'id': 1, 'name': u'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) From 3c7a63619900b92e9d642a7c3c048f9132b74cd4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 16:58:51 +0000 Subject: [PATCH 116/129] Updated release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f5d0fa077..3ca3e6b33 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -18,6 +18,7 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ### Master +* Bugfix: ModelSerializers now include reverse FK fields on creation. * Bugfix: Model fields with `blank=True` are now `required=False` by default. * Bugfix: Nested serializers now support nullable relationships. From 275fcde5e6f79e30b44e3f4e620b0328a0692532 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 16:59:01 +0000 Subject: [PATCH 117/129] Added @toranb - Thank you! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e4b8da4c8..eafea6d4d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -82,6 +82,7 @@ The following people have helped make REST framework great. * Joel Marcotte - [joual] * Trey Hunner - [treyhunner] * Roman Akinfold - [akinfold] +* Toran Billups - [toranb] Many thanks to everyone who's contributed to the project. @@ -199,3 +200,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [joual]: https://github.com/joual [treyhunner]: https://github.com/treyhunner [akinfold]: https://github.com/akinfold +[toranb]: https://github.com/toranb From 031a656667b1563516fa3cdb664da61243bbe00d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 17:12:07 +0000 Subject: [PATCH 118/129] Added hyperlinked FK create test --- rest_framework/tests/relations_hyperlink.py | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 86d7b1c23..721e975c5 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -234,6 +234,43 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_create(self): + data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'} + serializer = ForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + # Ensure source 1 is updated, and everything else is as expected + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, + {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'}, + ] + self.assertEquals(serializer.data, expected) + + # def test_reverse_foreign_key_create(self): + # data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + # serializer = ForeignKeyTargetSerializer(data=data) + # self.assertTrue(serializer.is_valid()) + # obj = serializer.save() + # self.assertEquals(serializer.data, data) + # self.assertEqual(obj.name, u'target-3') + + # # Ensure target 4 is added, and everything else is as expected + # queryset = ForeignKeyTarget.objects.all() + # serializer = ForeignKeyTargetSerializer(queryset) + # expected = [ + # {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, + # {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + # {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + # ] + # self.assertEquals(serializer.data, expected) + def test_foreign_key_update_with_invalid_null(self): data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) From 0176fab4758163476d5a12f8343043b4e7d325f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 17:12:40 +0000 Subject: [PATCH 119/129] Add TODO --- rest_framework/tests/relations_hyperlink.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 721e975c5..5eeabddb2 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -253,6 +253,7 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + # TODO: Check this - is this a bug or is the test incorrect? # def test_reverse_foreign_key_create(self): # data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} # serializer = ForeignKeyTargetSerializer(data=data) From 18590a1568aa8720c5114dbf40c6f69bcb753aaf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 18:17:48 +0000 Subject: [PATCH 120/129] Add test for hyperlinked reverse FK create. Refs #511 --- rest_framework/tests/relations_hyperlink.py | 35 ++++++++++----------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 5eeabddb2..f1de4c923 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -51,7 +51,7 @@ class ForeignKeySource(models.Model): class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer): - sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail', read_only=True) + sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail') class Meta: model = ForeignKeyTarget @@ -253,24 +253,23 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) - # TODO: Check this - is this a bug or is the test incorrect? - # def test_reverse_foreign_key_create(self): - # data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} - # serializer = ForeignKeyTargetSerializer(data=data) - # self.assertTrue(serializer.is_valid()) - # obj = serializer.save() - # self.assertEquals(serializer.data, data) - # self.assertEqual(obj.name, u'target-3') + def test_reverse_foreign_key_create(self): + data = {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + serializer = ForeignKeyTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'target-3') - # # Ensure target 4 is added, and everything else is as expected - # queryset = ForeignKeyTarget.objects.all() - # serializer = ForeignKeyTargetSerializer(queryset) - # expected = [ - # {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, - # {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, - # {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, - # ] - # self.assertEquals(serializer.data, expected) + # Ensure target 4 is added, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, + {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + {'url': '/foreignkeytarget/3/', 'name': u'target-3', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + ] + self.assertEquals(serializer.data, expected) def test_foreign_key_update_with_invalid_null(self): data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None} From 12c4f1ecf7dd7b0e6ead5a42c0fd5cab5aa7ad27 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Dec 2012 18:26:47 +0000 Subject: [PATCH 121/129] Add reverse FK update tests --- rest_framework/tests/relations_hyperlink.py | 17 +++++++++++++++++ rest_framework/tests/relations_pk.py | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index f1de4c923..240394105 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -234,6 +234,23 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_reverse_foreign_key_update(self): + data = {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']} + instance = ForeignKeyTarget.objects.get(pk=2) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 2 is update, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/2/']}, + {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/3/']}, + ] + self.assertEquals(serializer.data, expected) + def test_foreign_key_create(self): data = {'url': '/foreignkeysource/4/', 'name': u'source-4', 'target': '/foreignkeytarget/2/'} serializer = ForeignKeySourceSerializer(data=data) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index c2e61279a..01109ef95 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -216,6 +216,23 @@ class PKForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_reverse_foreign_key_update(self): + data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]} + instance = ForeignKeyTarget.objects.get(pk=2) + serializer = ForeignKeyTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure target 2 is update, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': [2]}, + {'id': 2, 'name': u'target-2', 'sources': [1, 3]}, + ] + self.assertEquals(serializer.data, expected) + def test_foreign_key_create(self): data = {'id': 4, 'name': u'source-4', 'target': 2} serializer = ForeignKeySourceSerializer(data=data) From b1e6b58c9729c77dda3b73b47142eff3d29935c9 Mon Sep 17 00:00:00 2001 From: Sebastien Beal Date: Sun, 30 Dec 2012 13:24:05 +0900 Subject: [PATCH 122/129] missing rest_framework templatetags for statics in login template --- rest_framework/templates/rest_framework/login.html | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index e3f3d799b..6e2bd8d4d 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -1,4 +1,5 @@ {% load url from future %} +{% load rest_framework %} From af5c3c3bf4645fa9cb1d7c4d17c0f3a530040df8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 30 Dec 2012 07:51:07 +0000 Subject: [PATCH 123/129] Added @sebastibe. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index eafea6d4d..c4277a23d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -83,6 +83,7 @@ The following people have helped make REST framework great. * Trey Hunner - [treyhunner] * Roman Akinfold - [akinfold] * Toran Billups - [toranb] +* Sébastien Béal - [sebastibe] Many thanks to everyone who's contributed to the project. @@ -201,3 +202,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [treyhunner]: https://github.com/treyhunner [akinfold]: https://github.com/akinfold [toranb]: https://github.com/toranb +[sebastibe]: https://github.com/sebastibe From 33580c82b3487bdf00cbbaef409a4dd41e6750d5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 30 Dec 2012 08:06:04 +0000 Subject: [PATCH 124/129] Cleanup runtests/runcoverage --- rest_framework/runtests/runcoverage.py | 9 +++++++++ rest_framework/runtests/runtests.py | 8 +++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py index 0ce379ebe..bcab1d148 100755 --- a/rest_framework/runtests/runcoverage.py +++ b/rest_framework/runtests/runcoverage.py @@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py import os import sys + +# fix sys path so we don't need to setup PYTHONPATH +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' from coverage import coverage @@ -55,6 +58,12 @@ def main(): if 'compat.py' in files: files.remove('compat.py') + # Same applies to template tags module. + # This module has to include branching on Django versions, + # so it's never possible for it to have full coverage. + if 'rest_framework.py' in files: + files.remove('rest_framework.py') + cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')]) cov.report(cov_files) diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index 729ef26ac..505994e28 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -5,11 +5,9 @@ # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py import os import sys -""" -Need to fix sys path so following works without specifically messing with PYTHONPATH -python ./rest_framework/runtests/runtests.py -""" -sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) + +# fix sys path so we don't need to setup PYTHONPATH +sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' from django.conf import settings From 8fad0a727a897970531a087346ecd44f361b25f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 31 Dec 2012 08:53:40 +0000 Subject: [PATCH 125/129] Relation fields move into relations.py --- docs/api-guide/fields.md | 179 ++------------ docs/api-guide/relations.md | 139 +++++++++++ docs/index.md | 2 + docs/template.html | 1 + rest_framework/fields.py | 443 +-------------------------------- rest_framework/relations.py | 446 ++++++++++++++++++++++++++++++++++ rest_framework/serializers.py | 2 +- 7 files changed, 617 insertions(+), 595 deletions(-) create mode 100644 docs/api-guide/relations.md create mode 100644 rest_framework/relations.py diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 50a09701e..5bc8f7f7c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -2,11 +2,11 @@ # Serializer fields -> Flat is better than nested. +> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it -- normalizing it to a consistent format. > -> — [The Zen of Python][cite] +> — [Django documentation][cite] -Serializer fields handle converting between primative values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects. +Serializer fields handle converting between primitive values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects. --- @@ -28,7 +28,7 @@ Defaults to the name of the field. ### `read_only` -Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance dureing deserialization. +Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization. Defaults to `False` @@ -41,7 +41,7 @@ Defaults to `True`. ### `default` -If set, this gives the default value that will be used for the field if none is supplied. If not set the default behaviour is to not populate the attribute at all. +If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all. ### `validators` @@ -96,9 +96,9 @@ Would produce output similar to: 'expired': True } -By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary. +By default, the `Field` class will perform a basic translation of the source value into primitive datatypes, falling back to unicode representations of complex datatypes when necessary. -You can customize this behaviour by overriding the `.to_native(self, value)` method. +You can customize this behavior by overriding the `.to_native(self, value)` method. ## WritableField @@ -110,6 +110,24 @@ A generic field that can be tied to any arbitrary model field. The `ModelField` **Signature:** `ModelField(model_field=)` +## SerializerMethodField + +This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: + + from rest_framework import serializers + from django.contrib.auth.models import User + from django.utils.timezone import now + + class UserSerializer(serializers.ModelSerializer): + + days_since_joined = serializers.SerializerMethodField('get_days_since_joined') + + class Meta: + model = User + + def get_days_since_joined(self, obj): + return (now() - obj.date_joined).days + --- # Typed Fields @@ -211,151 +229,8 @@ Signature and validation is the same as with `FileField`. --- -**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since eg json doesn't support file uploads. +**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads. Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. ---- - -# Relational Fields - -Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`. - -## RelatedField - -This field can be applied to any of the following: - -* A `ForeignKey` field. -* A `OneToOneField` field. -* A reverse OneToOne relationship -* Any other "to-one" relationship. - -By default `RelatedField` will represent the target of the field using it's `__unicode__` method. - -You can customise this behaviour by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method. - -## ManyRelatedField - -This field can be applied to any of the following: - -* A `ManyToManyField` field. -* A reverse ManyToMany relationship. -* A reverse ForeignKey relationship -* Any other "to-many" relationship. - -By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method. - -For example, given the following models: - - class TaggedItem(models.Model): - """ - Tags arbitrary model instances using a generic relation. - - See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ - """ - tag = models.SlugField() - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = GenericForeignKey('content_type', 'object_id') - - def __unicode__(self): - return self.tag - - - class Bookmark(models.Model): - """ - A bookmark consists of a URL, and 0 or more descriptive tags. - """ - url = models.URLField() - tags = GenericRelation(TaggedItem) - -And a model serializer defined like this: - - class BookmarkSerializer(serializers.ModelSerializer): - tags = serializers.ManyRelatedField(source='tags') - - class Meta: - model = Bookmark - exclude = ('id',) - -Then an example output format for a Bookmark instance would be: - - { - 'tags': [u'django', u'python'], - 'url': u'https://www.djangoproject.com/' - } - -## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField - -`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key. - -By default these fields are read-write, although you can change this behaviour using the `read_only` flag. - -**Arguments**: - -* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. -* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. - -## SlugRelatedField / ManySlugRelatedField - -`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug. - -By default these fields read-write, although you can change this behaviour using the `read_only` flag. - -**Arguments**: - -* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. -* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. -* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. - -## HyperlinkedRelatedField / ManyHyperlinkedRelatedField - -`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink. - -By default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag. - -**Arguments**: - -* `view_name` - The view name that should be used as the target of the relationship. **required**. -* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. -* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. -* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. -* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. -* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. -* `null` - If set to `True`, the field will accept values of `None` or the emptystring for nullable relationships. - -## HyperLinkedIdentityField - -This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. - -This field is always read-only. - -**Arguments**: - -* `view_name` - The view name that should be used as the target of the relationship. **required**. -* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. -* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. -* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. -* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. - -# Other Fields - -## SerializerMethodField - -This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: - - from rest_framework import serializers - from django.contrib.auth.models import User - from django.utils.timezone import now - - class UserSerializer(serializers.ModelSerializer): - - days_since_joined = serializers.SerializerMethodField('get_days_since_joined') - - class Meta: - model = User - - def get_days_since_joined(self, obj): - return (now() - obj.date_joined).days - -[cite]: http://www.python.org/dev/peps/pep-0020/ +[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md new file mode 100644 index 000000000..351b5e09e --- /dev/null +++ b/docs/api-guide/relations.md @@ -0,0 +1,139 @@ + + +# Serializer relations + +> Bad programmers worry about the code. +> Good programmers worry about data structures and their relationships. +> +> — [Linus Torvalds][cite] + + +Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`. + +--- + +**Note:** The relational fields are declared in `relations.py`, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.`. + +--- + +## RelatedField + +This field can be applied to any of the following: + +* A `ForeignKey` field. +* A `OneToOneField` field. +* A reverse OneToOne relationship +* Any other "to-one" relationship. + +By default `RelatedField` will represent the target of the field using it's `__unicode__` method. + +You can customize this behavior by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method. + +## ManyRelatedField + +This field can be applied to any of the following: + +* A `ManyToManyField` field. +* A reverse ManyToMany relationship. +* A reverse ForeignKey relationship +* Any other "to-many" relationship. + +By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method. + +For example, given the following models: + + class TaggedItem(models.Model): + """ + Tags arbitrary model instances using a generic relation. + + See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ + """ + tag = models.SlugField() + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + def __unicode__(self): + return self.tag + + + class Bookmark(models.Model): + """ + A bookmark consists of a URL, and 0 or more descriptive tags. + """ + url = models.URLField() + tags = GenericRelation(TaggedItem) + +And a model serializer defined like this: + + class BookmarkSerializer(serializers.ModelSerializer): + tags = serializers.ManyRelatedField(source='tags') + + class Meta: + model = Bookmark + exclude = ('id',) + +Then an example output format for a Bookmark instance would be: + + { + 'tags': [u'django', u'python'], + 'url': u'https://www.djangoproject.com/' + } + +## PrimaryKeyRelatedField +## ManyPrimaryKeyRelatedField + +`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key. + +By default these fields are read-write, although you can change this behavior using the `read_only` flag. + +**Arguments**: + +* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships. + +## SlugRelatedField +## ManySlugRelatedField + +`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug. + +By default these fields read-write, although you can change this behavior using the `read_only` flag. + +**Arguments**: + +* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. +* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships. + +## HyperlinkedRelatedField +## ManyHyperlinkedRelatedField + +`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink. + +By default, `HyperlinkedRelatedField` is read-write, although you can change this behavior using the `read_only` flag. + +**Arguments**: + +* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. +* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`. +* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. +* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. +* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. +* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships. + +## HyperLinkedIdentityField + +This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. + +This field is always read-only. + +**Arguments**: + +* `view_name` - The view name that should be used as the target of the relationship. **required**. +* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument. +* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`. +* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`. +* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`. + +[cite]: http://lwn.net/Articles/193245/ diff --git a/docs/index.md b/docs/index.md index 69d972d0e..4d50e5d65 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,6 +94,7 @@ The API guide is your complete reference manual to all the functionality provide * [Renderers][renderers] * [Serializers][serializers] * [Serializer fields][fields] +* [Serializer relations][relations] * [Authentication][authentication] * [Permissions][permissions] * [Throttling][throttling] @@ -185,6 +186,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [renderers]: api-guide/renderers.md [serializers]: api-guide/serializers.md [fields]: api-guide/fields.md +[relations]: api-guide/relations.md [authentication]: api-guide/authentication.md [permissions]: api-guide/permissions.md [throttling]: api-guide/throttling.md diff --git a/docs/template.html b/docs/template.html index 676a48070..d789cc582 100644 --- a/docs/template.html +++ b/docs/template.html @@ -72,6 +72,7 @@
  • Renderers
  • Serializers
  • Serializer fields
  • +
  • Serializer relations
  • Authentication
  • Permissions
  • Throttling
  • diff --git a/rest_framework/fields.py b/rest_framework/fields.py index dd90c3f86..d8b82e5fb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -7,18 +7,14 @@ 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 +from django.core.exceptions import ValidationError from django.conf import settings from django import forms from django.forms import widgets -from django.forms.models import ModelChoiceIterator from django.utils.encoding import is_protected_type, smart_unicode from django.utils.translation import ugettext_lazy as _ -from rest_framework.reverse import reverse from rest_framework.compat import parse_date, parse_datetime from rest_framework.compat import timezone -from urlparse import urlparse def is_simple_callable(obj): @@ -252,443 +248,6 @@ class ModelField(WritableField): "type": self.model_field.get_internal_type() } -##### Relational fields ##### - - -# Not actually Writable, but subclasses may need to be. -class RelatedField(WritableField): - """ - Base class for related model fields. - - If not overridden, this represents a to-one relationship, using the unicode - representation of the target. - """ - widget = widgets.Select - cache_choices = False - empty_label = None - default_read_only = True # TODO: Remove this - - def __init__(self, *args, **kwargs): - self.queryset = kwargs.pop('queryset', None) - self.null = kwargs.pop('null', False) - super(RelatedField, self).__init__(*args, **kwargs) - self.read_only = kwargs.pop('read_only', self.default_read_only) - - def initialize(self, parent, field_name): - super(RelatedField, self).initialize(parent, field_name) - if self.queryset is None and not self.read_only: - try: - manager = getattr(self.parent.opts.model, self.source or field_name) - if hasattr(manager, 'related'): # Forward - self.queryset = manager.related.model._default_manager.all() - else: # Reverse - self.queryset = manager.field.rel.to._default_manager.all() - except: - raise - msg = ('Serializer related fields must include a `queryset`' + - ' argument or set `read_only=True') - raise Exception(msg) - - ### We need this stuff to make form choices work... - - # def __deepcopy__(self, memo): - # result = super(RelatedField, self).__deepcopy__(memo) - # result.queryset = result.queryset - # return result - - def prepare_value(self, obj): - return self.to_native(obj) - - def label_from_instance(self, obj): - """ - Return a readable representation for use with eg. select widgets. - """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj)) - if desc == ident: - return desc - return "%s - %s" % (desc, ident) - - def _get_queryset(self): - return self._queryset - - def _set_queryset(self, queryset): - self._queryset = queryset - self.widget.choices = self.choices - - queryset = property(_get_queryset, _set_queryset) - - def _get_choices(self): - # If self._choices is set, then somebody must have manually set - # the property self.choices. In this case, just return self._choices. - if hasattr(self, '_choices'): - return self._choices - - # Otherwise, execute the QuerySet in self.queryset to determine the - # choices dynamically. Return a fresh ModelChoiceIterator that has not been - # consumed. Note that we're instantiating a new ModelChoiceIterator *each* - # time _get_choices() is called (and, thus, each time self.choices is - # accessed) so that we can ensure the QuerySet has not been consumed. This - # construct might look complicated but it allows for lazy evaluation of - # the queryset. - return ModelChoiceIterator(self) - - def _set_choices(self, value): - # Setting choices also sets the choices on the widget. - # choices can be any iterable, but we call list() on it because - # it will be consumed more than once. - self._choices = self.widget.choices = list(value) - - choices = property(_get_choices, _set_choices) - - ### Regular serializer stuff... - - def field_to_native(self, obj, field_name): - value = getattr(obj, self.source or field_name) - return self.to_native(value) - - def field_from_native(self, data, files, field_name, into): - if self.read_only: - return - - try: - value = data[field_name] - except KeyError: - if self.required: - raise ValidationError(self.error_messages['required']) - return - - if value in (None, '') and not self.null: - raise ValidationError('Value may not be null') - elif value in (None, '') and self.null: - into[(self.source or field_name)] = None - else: - into[(self.source or field_name)] = self.from_native(value) - - -class ManyRelatedMixin(object): - """ - Mixin to convert a related field to a many related field. - """ - widget = widgets.SelectMultiple - - def field_to_native(self, obj, field_name): - value = getattr(obj, self.source or field_name) - return [self.to_native(item) for item in value.all()] - - def field_from_native(self, data, files, field_name, into): - if self.read_only: - return - - try: - # Form data - value = data.getlist(self.source or field_name) - except: - # Non-form data - value = data.get(self.source or field_name) - else: - if value == ['']: - value = [] - - into[field_name] = [self.from_native(item) for item in value] - - -class ManyRelatedField(ManyRelatedMixin, RelatedField): - """ - Base class for related model managers. - - If not overridden, this represents a to-many relationship, using the unicode - representations of the target, and is read-only. - """ - pass - - -### PrimaryKey relationships - -class PrimaryKeyRelatedField(RelatedField): - """ - Represents a to-one relationship as a pk value. - """ - default_read_only = False - form_field_class = forms.ChoiceField - - # TODO: Remove these field hacks... - def prepare_value(self, obj): - return self.to_native(obj.pk) - - def label_from_instance(self, obj): - """ - Return a readable representation for use with eg. select widgets. - """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) - if desc == ident: - return desc - return "%s - %s" % (desc, ident) - - # TODO: Possibly change this to just take `obj`, through prob less performant - def to_native(self, pk): - return pk - - def from_native(self, data): - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') - - try: - return self.queryset.get(pk=data) - except ObjectDoesNotExist: - msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) - raise ValidationError(msg) - - def field_to_native(self, obj, field_name): - try: - # Prefer obj.serializable_value for performance reasons - pk = obj.serializable_value(self.source or field_name) - except AttributeError: - # RelatedObject (reverse relationship) - obj = getattr(obj, self.source or field_name) - return self.to_native(obj.pk) - # Forward relationship - return self.to_native(pk) - - -class ManyPrimaryKeyRelatedField(ManyRelatedField): - """ - Represents a to-many relationship as a pk value. - """ - default_read_only = False - form_field_class = forms.MultipleChoiceField - - def prepare_value(self, obj): - return self.to_native(obj.pk) - - def label_from_instance(self, obj): - """ - Return a readable representation for use with eg. select widgets. - """ - desc = smart_unicode(obj) - ident = smart_unicode(self.to_native(obj.pk)) - if desc == ident: - return desc - return "%s - %s" % (desc, ident) - - def to_native(self, pk): - return pk - - def field_to_native(self, obj, field_name): - try: - # Prefer obj.serializable_value for performance reasons - queryset = obj.serializable_value(self.source or field_name) - except AttributeError: - # RelatedManager (reverse relationship) - queryset = getattr(obj, self.source or field_name) - return [self.to_native(item.pk) for item in queryset.all()] - # Forward relationship - return [self.to_native(item.pk) for item in queryset.all()] - - def from_native(self, data): - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') - - try: - return self.queryset.get(pk=data) - except ObjectDoesNotExist: - msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) - raise ValidationError(msg) - -### Slug relationships - - -class SlugRelatedField(RelatedField): - default_read_only = False - form_field_class = forms.ChoiceField - - def __init__(self, *args, **kwargs): - self.slug_field = kwargs.pop('slug_field', None) - assert self.slug_field, 'slug_field is required' - super(SlugRelatedField, self).__init__(*args, **kwargs) - - def to_native(self, obj): - return getattr(obj, self.slug_field) - - def from_native(self, data): - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') - - try: - return self.queryset.get(**{self.slug_field: data}) - except ObjectDoesNotExist: - raise ValidationError('Object with %s=%s does not exist.' % - (self.slug_field, unicode(data))) - - -class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField): - form_field_class = forms.MultipleChoiceField - - -### Hyperlinked relationships - -class HyperlinkedRelatedField(RelatedField): - """ - Represents a to-one relationship, using hyperlinking. - """ - pk_url_kwarg = 'pk' - slug_field = 'slug' - slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden - default_read_only = False - form_field_class = forms.ChoiceField - - def __init__(self, *args, **kwargs): - try: - self.view_name = kwargs.pop('view_name') - except: - raise ValueError("Hyperlinked field requires 'view_name' kwarg") - - self.slug_field = kwargs.pop('slug_field', self.slug_field) - default_slug_kwarg = self.slug_url_kwarg or self.slug_field - self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) - self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) - - self.format = kwargs.pop('format', None) - super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) - - def get_slug_field(self): - """ - Get the name of a slug field to be used to look up by slug. - """ - return self.slug_field - - def to_native(self, obj): - view_name = self.view_name - request = self.context.get('request', None) - format = self.format or self.context.get('format', None) - pk = getattr(obj, 'pk', None) - if pk is None: - return - kwargs = {self.pk_url_kwarg: pk} - try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - slug = getattr(obj, self.slug_field, None) - - if not slug: - raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) - - kwargs = {self.slug_url_kwarg: slug} - try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} - try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) - - def from_native(self, value): - # Convert URL -> model instance pk - # TODO: Use values_list - if self.queryset is None: - raise Exception('Writable related fields must include a `queryset` argument') - - if value.startswith('http:') or value.startswith('https:'): - # If needed convert absolute URLs to relative path - value = urlparse(value).path - prefix = get_script_prefix() - if value.startswith(prefix): - value = '/' + value[len(prefix):] - - try: - match = resolve(value) - except: - raise ValidationError('Invalid hyperlink - No URL match') - - if match.url_name != self.view_name: - raise ValidationError('Invalid hyperlink - Incorrect URL match') - - pk = match.kwargs.get(self.pk_url_kwarg, None) - slug = match.kwargs.get(self.slug_url_kwarg, None) - - # Try explicit primary key. - if pk is not None: - queryset = self.queryset.filter(pk=pk) - # Next, try looking up by slug. - elif slug is not None: - slug_field = self.get_slug_field() - queryset = self.queryset.filter(**{slug_field: slug}) - # If none of those are defined, it's an error. - else: - raise ValidationError('Invalid hyperlink') - - try: - obj = queryset.get() - except ObjectDoesNotExist: - raise ValidationError('Invalid hyperlink - object does not exist.') - return obj - - -class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): - """ - Represents a to-many relationship, using hyperlinking. - """ - form_field_class = forms.MultipleChoiceField - - -class HyperlinkedIdentityField(Field): - """ - Represents the instance, or a property on the instance, using hyperlinking. - """ - pk_url_kwarg = 'pk' - slug_field = 'slug' - slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden - - def __init__(self, *args, **kwargs): - # TODO: Make view_name mandatory, and have the - # HyperlinkedModelSerializer set it on-the-fly - self.view_name = kwargs.pop('view_name', None) - self.format = kwargs.pop('format', None) - - self.slug_field = kwargs.pop('slug_field', self.slug_field) - default_slug_kwarg = self.slug_url_kwarg or self.slug_field - self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) - self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) - - super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) - - def field_to_native(self, obj, field_name): - request = self.context.get('request', None) - format = self.format or self.context.get('format', None) - view_name = self.view_name or self.parent.opts.view_name - kwargs = {self.pk_url_kwarg: obj.pk} - try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - slug = getattr(obj, self.slug_field, None) - - if not slug: - raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) - - kwargs = {self.slug_url_kwarg: slug} - try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} - try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) - except: - pass - - raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) - ##### Typed Fields ##### diff --git a/rest_framework/relations.py b/rest_framework/relations.py new file mode 100644 index 000000000..9b3a77909 --- /dev/null +++ b/rest_framework/relations.py @@ -0,0 +1,446 @@ +from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.urlresolvers import resolve, get_script_prefix +from django import forms +from django.forms import widgets +from django.forms.models import ModelChoiceIterator +from django.utils.encoding import smart_unicode +from rest_framework.fields import Field, WritableField +from rest_framework.reverse import reverse +from urlparse import urlparse + +##### Relational fields ##### + + +# Not actually Writable, but subclasses may need to be. +class RelatedField(WritableField): + """ + Base class for related model fields. + + If not overridden, this represents a to-one relationship, using the unicode + representation of the target. + """ + widget = widgets.Select + cache_choices = False + empty_label = None + default_read_only = True # TODO: Remove this + + def __init__(self, *args, **kwargs): + self.queryset = kwargs.pop('queryset', None) + self.null = kwargs.pop('null', False) + super(RelatedField, self).__init__(*args, **kwargs) + self.read_only = kwargs.pop('read_only', self.default_read_only) + + def initialize(self, parent, field_name): + super(RelatedField, self).initialize(parent, field_name) + if self.queryset is None and not self.read_only: + try: + manager = getattr(self.parent.opts.model, self.source or field_name) + if hasattr(manager, 'related'): # Forward + self.queryset = manager.related.model._default_manager.all() + else: # Reverse + self.queryset = manager.field.rel.to._default_manager.all() + except: + raise + msg = ('Serializer related fields must include a `queryset`' + + ' argument or set `read_only=True') + raise Exception(msg) + + ### We need this stuff to make form choices work... + + # def __deepcopy__(self, memo): + # result = super(RelatedField, self).__deepcopy__(memo) + # result.queryset = result.queryset + # return result + + def prepare_value(self, obj): + return self.to_native(obj) + + def label_from_instance(self, obj): + """ + Return a readable representation for use with eg. select widgets. + """ + desc = smart_unicode(obj) + ident = smart_unicode(self.to_native(obj)) + if desc == ident: + return desc + return "%s - %s" % (desc, ident) + + def _get_queryset(self): + return self._queryset + + def _set_queryset(self, queryset): + self._queryset = queryset + self.widget.choices = self.choices + + queryset = property(_get_queryset, _set_queryset) + + def _get_choices(self): + # If self._choices is set, then somebody must have manually set + # the property self.choices. In this case, just return self._choices. + if hasattr(self, '_choices'): + return self._choices + + # Otherwise, execute the QuerySet in self.queryset to determine the + # choices dynamically. Return a fresh ModelChoiceIterator that has not been + # consumed. Note that we're instantiating a new ModelChoiceIterator *each* + # time _get_choices() is called (and, thus, each time self.choices is + # accessed) so that we can ensure the QuerySet has not been consumed. This + # construct might look complicated but it allows for lazy evaluation of + # the queryset. + return ModelChoiceIterator(self) + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + ### Regular serializer stuff... + + def field_to_native(self, obj, field_name): + value = getattr(obj, self.source or field_name) + return self.to_native(value) + + def field_from_native(self, data, files, field_name, into): + if self.read_only: + return + + try: + value = data[field_name] + except KeyError: + if self.required: + raise ValidationError(self.error_messages['required']) + return + + if value in (None, '') and not self.null: + raise ValidationError('Value may not be null') + elif value in (None, '') and self.null: + into[(self.source or field_name)] = None + else: + into[(self.source or field_name)] = self.from_native(value) + + +class ManyRelatedMixin(object): + """ + Mixin to convert a related field to a many related field. + """ + widget = widgets.SelectMultiple + + def field_to_native(self, obj, field_name): + value = getattr(obj, self.source or field_name) + return [self.to_native(item) for item in value.all()] + + def field_from_native(self, data, files, field_name, into): + if self.read_only: + return + + try: + # Form data + value = data.getlist(self.source or field_name) + except: + # Non-form data + value = data.get(self.source or field_name) + else: + if value == ['']: + value = [] + + into[field_name] = [self.from_native(item) for item in value] + + +class ManyRelatedField(ManyRelatedMixin, RelatedField): + """ + Base class for related model managers. + + If not overridden, this represents a to-many relationship, using the unicode + representations of the target, and is read-only. + """ + pass + + +### PrimaryKey relationships + +class PrimaryKeyRelatedField(RelatedField): + """ + Represents a to-one relationship as a pk value. + """ + default_read_only = False + form_field_class = forms.ChoiceField + + # TODO: Remove these field hacks... + def prepare_value(self, obj): + return self.to_native(obj.pk) + + def label_from_instance(self, obj): + """ + Return a readable representation for use with eg. select widgets. + """ + desc = smart_unicode(obj) + ident = smart_unicode(self.to_native(obj.pk)) + if desc == ident: + return desc + return "%s - %s" % (desc, ident) + + # TODO: Possibly change this to just take `obj`, through prob less performant + def to_native(self, pk): + return pk + + def from_native(self, data): + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + try: + return self.queryset.get(pk=data) + except ObjectDoesNotExist: + msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) + raise ValidationError(msg) + + def field_to_native(self, obj, field_name): + try: + # Prefer obj.serializable_value for performance reasons + pk = obj.serializable_value(self.source or field_name) + except AttributeError: + # RelatedObject (reverse relationship) + obj = getattr(obj, self.source or field_name) + return self.to_native(obj.pk) + # Forward relationship + return self.to_native(pk) + + +class ManyPrimaryKeyRelatedField(ManyRelatedField): + """ + Represents a to-many relationship as a pk value. + """ + default_read_only = False + form_field_class = forms.MultipleChoiceField + + def prepare_value(self, obj): + return self.to_native(obj.pk) + + def label_from_instance(self, obj): + """ + Return a readable representation for use with eg. select widgets. + """ + desc = smart_unicode(obj) + ident = smart_unicode(self.to_native(obj.pk)) + if desc == ident: + return desc + return "%s - %s" % (desc, ident) + + def to_native(self, pk): + return pk + + def field_to_native(self, obj, field_name): + try: + # Prefer obj.serializable_value for performance reasons + queryset = obj.serializable_value(self.source or field_name) + except AttributeError: + # RelatedManager (reverse relationship) + queryset = getattr(obj, self.source or field_name) + return [self.to_native(item.pk) for item in queryset.all()] + # Forward relationship + return [self.to_native(item.pk) for item in queryset.all()] + + def from_native(self, data): + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + try: + return self.queryset.get(pk=data) + except ObjectDoesNotExist: + msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data) + raise ValidationError(msg) + +### Slug relationships + + +class SlugRelatedField(RelatedField): + default_read_only = False + form_field_class = forms.ChoiceField + + def __init__(self, *args, **kwargs): + self.slug_field = kwargs.pop('slug_field', None) + assert self.slug_field, 'slug_field is required' + super(SlugRelatedField, self).__init__(*args, **kwargs) + + def to_native(self, obj): + return getattr(obj, self.slug_field) + + def from_native(self, data): + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + try: + return self.queryset.get(**{self.slug_field: data}) + except ObjectDoesNotExist: + raise ValidationError('Object with %s=%s does not exist.' % + (self.slug_field, unicode(data))) + + +class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField): + form_field_class = forms.MultipleChoiceField + + +### Hyperlinked relationships + +class HyperlinkedRelatedField(RelatedField): + """ + Represents a to-one relationship, using hyperlinking. + """ + pk_url_kwarg = 'pk' + slug_field = 'slug' + slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden + default_read_only = False + form_field_class = forms.ChoiceField + + def __init__(self, *args, **kwargs): + try: + self.view_name = kwargs.pop('view_name') + except: + raise ValueError("Hyperlinked field requires 'view_name' kwarg") + + self.slug_field = kwargs.pop('slug_field', self.slug_field) + default_slug_kwarg = self.slug_url_kwarg or self.slug_field + self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) + self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) + + self.format = kwargs.pop('format', None) + super(HyperlinkedRelatedField, self).__init__(*args, **kwargs) + + def get_slug_field(self): + """ + Get the name of a slug field to be used to look up by slug. + """ + return self.slug_field + + def to_native(self, obj): + view_name = self.view_name + request = self.context.get('request', None) + format = self.format or self.context.get('format', None) + pk = getattr(obj, 'pk', None) + if pk is None: + return + kwargs = {self.pk_url_kwarg: pk} + try: + return reverse(view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + slug = getattr(obj, self.slug_field, None) + + if not slug: + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) + + kwargs = {self.slug_url_kwarg: slug} + try: + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} + try: + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) + + def from_native(self, value): + # Convert URL -> model instance pk + # TODO: Use values_list + if self.queryset is None: + raise Exception('Writable related fields must include a `queryset` argument') + + if value.startswith('http:') or value.startswith('https:'): + # If needed convert absolute URLs to relative path + value = urlparse(value).path + prefix = get_script_prefix() + if value.startswith(prefix): + value = '/' + value[len(prefix):] + + try: + match = resolve(value) + except: + raise ValidationError('Invalid hyperlink - No URL match') + + if match.url_name != self.view_name: + raise ValidationError('Invalid hyperlink - Incorrect URL match') + + pk = match.kwargs.get(self.pk_url_kwarg, None) + slug = match.kwargs.get(self.slug_url_kwarg, None) + + # Try explicit primary key. + if pk is not None: + queryset = self.queryset.filter(pk=pk) + # Next, try looking up by slug. + elif slug is not None: + slug_field = self.get_slug_field() + queryset = self.queryset.filter(**{slug_field: slug}) + # If none of those are defined, it's an error. + else: + raise ValidationError('Invalid hyperlink') + + try: + obj = queryset.get() + except ObjectDoesNotExist: + raise ValidationError('Invalid hyperlink - object does not exist.') + return obj + + +class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField): + """ + Represents a to-many relationship, using hyperlinking. + """ + form_field_class = forms.MultipleChoiceField + + +class HyperlinkedIdentityField(Field): + """ + Represents the instance, or a property on the instance, using hyperlinking. + """ + pk_url_kwarg = 'pk' + slug_field = 'slug' + slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden + + def __init__(self, *args, **kwargs): + # TODO: Make view_name mandatory, and have the + # HyperlinkedModelSerializer set it on-the-fly + self.view_name = kwargs.pop('view_name', None) + self.format = kwargs.pop('format', None) + + self.slug_field = kwargs.pop('slug_field', self.slug_field) + default_slug_kwarg = self.slug_url_kwarg or self.slug_field + self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg) + self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg) + + super(HyperlinkedIdentityField, self).__init__(*args, **kwargs) + + def field_to_native(self, obj, field_name): + request = self.context.get('request', None) + format = self.format or self.context.get('format', None) + view_name = self.view_name or self.parent.opts.view_name + kwargs = {self.pk_url_kwarg: obj.pk} + try: + return reverse(view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + slug = getattr(obj, self.slug_field, None) + + if not slug: + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) + + kwargs = {self.slug_url_kwarg: slug} + try: + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug} + try: + return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + except: + pass + + raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e8e6735a5..ed173d85c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,7 @@ from rest_framework.compat import get_concrete_model # This helps keep the seperation between model fields, form fields, and # serializer fields more explicit. - +from rest_framework.relations import * from rest_framework.fields import * From eff833b39d2f41c9eb773214f5b45c3d991e1511 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 31 Dec 2012 08:53:49 +0000 Subject: [PATCH 126/129] Version 2.1.14 --- README.md | 12 ++++++++++++ docs/topics/release-notes.md | 9 ++++++++- rest_framework/__init__.py | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c262cd684..e1d85f3cb 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,18 @@ To run the tests. # Changelog +### 2.1.14 + +**Date**: 31st Dec 2012 + +* Bugfix: ModelSerializers now include reverse FK fields on creation. +* Bugfix: Model fields with `blank=True` are now `required=False` by default. +* Bugfix: Nested serializers now support nullable relationships. + +**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`. + +This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`. + ### 2.1.13 **Date**: 28th Dec 2012 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3ca3e6b33..c93eebac8 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -16,12 +16,19 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ## 2.1.x series -### Master +### 2.1.14 + +**Date**: 31st Dec 2012 * Bugfix: ModelSerializers now include reverse FK fields on creation. * Bugfix: Model fields with `blank=True` are now `required=False` by default. * Bugfix: Nested serializers now support nullable relationships. +**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to seperate them from regular data type fields, such as `CharField` and `IntegerField`. + +This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and refering to fields using the style `serializers.PrimaryKeyRelatedField`. + + ### 2.1.13 **Date**: 28th Dec 2012 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2e38d8639..151ba8324 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.13' +__version__ = '2.1.14' VERSION = __version__ # synonym From a617a3758f635bf1ebe3464555e397d09f4dfc6d Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Mon, 31 Dec 2012 14:33:24 +0100 Subject: [PATCH 127/129] Don't persist relation changes in ModelSerializer#restore_object() --- rest_framework/serializers.py | 34 ++++++++++----------- rest_framework/tests/relations_hyperlink.py | 6 ++-- rest_framework/tests/relations_pk.py | 20 +++++++++--- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ed173d85c..24674f2a6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -498,28 +498,28 @@ class ModelSerializer(Serializer): self.m2m_data = {} self.related_data = {} + # Reverse fk relations + for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model(): + field_name = obj.field.related_query_name() + if field_name in attrs: + self.related_data[field_name] = attrs.pop(field_name) + + # Reverse m2m relations + for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): + field_name = obj.field.related_query_name() + if field_name in attrs: + self.m2m_data[field_name] = attrs.pop(field_name) + + # Forward m2m relations + for field in self.opts.model._meta.many_to_many: + if field.name in attrs: + self.m2m_data[field.name] = attrs.pop(field.name) + if instance is not None: for key, val in attrs.items(): setattr(instance, key, val) else: - # Reverse fk relations - for (obj, model) in self.opts.model._meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() - if field_name in attrs: - self.related_data[field_name] = attrs.pop(field_name) - - # Reverse m2m relations - for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): - field_name = obj.field.related_query_name() - if field_name in attrs: - self.m2m_data[field_name] = attrs.pop(field_name) - - # Forward m2m relations - for field in self.opts.model._meta.many_to_many: - if field.name in attrs: - self.m2m_data[field.name] = attrs.pop(field.name) - instance = self.opts.model(**attrs) try: diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 240394105..4179ec680 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -114,8 +114,8 @@ class HyperlinkedManyToManyTests(TestCase): instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() + self.assertEquals(serializer.data, data) # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -132,8 +132,8 @@ class HyperlinkedManyToManyTests(TestCase): instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() + self.assertEquals(serializer.data, data) # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -239,8 +239,8 @@ class HyperlinkedForeignKeyTests(TestCase): instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() + self.assertEquals(serializer.data, data) # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 01109ef95..289670999 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -99,8 +99,8 @@ class PKManyToManyTests(TestCase): instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() + self.assertEquals(serializer.data, data) # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -117,8 +117,8 @@ class PKManyToManyTests(TestCase): instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) serializer.save() + self.assertEquals(serializer.data, data) # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -221,8 +221,18 @@ class PKForeignKeyTests(TestCase): instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, data) + # We shouldn't have saved anything to the db yet since save + # hasn't been called. + queryset = ForeignKeyTarget.objects.all() + new_serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]}, + {'id': 2, 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(new_serializer.data, expected) + serializer.save() + self.assertEquals(serializer.data, data) # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() @@ -241,7 +251,7 @@ class PKForeignKeyTests(TestCase): self.assertEquals(serializer.data, data) self.assertEqual(obj.name, u'source-4') - # Ensure source 1 is updated, and everything else is as expected + # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() serializer = ForeignKeySourceSerializer(queryset) expected = [ @@ -260,7 +270,7 @@ class PKForeignKeyTests(TestCase): self.assertEquals(serializer.data, data) self.assertEqual(obj.name, u'target-3') - # Ensure target 4 is added, and everything else is as expected + # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() serializer = ForeignKeyTargetSerializer(queryset) expected = [ From 44771e81b23ca8ef982e4bb0d0ac5a435b684b22 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Tue, 1 Jan 2013 17:51:39 +0100 Subject: [PATCH 128/129] Update HyperlinkedForeignKeyTests to match PKForeignKeyTests --- rest_framework/tests/relations_hyperlink.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 4179ec680..0a7ea0f48 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -239,6 +239,16 @@ class HyperlinkedForeignKeyTests(TestCase): instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) + # We shouldn't have saved anything to the db yet since save + # hasn't been called. + queryset = ForeignKeyTarget.objects.all() + new_serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, + {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(new_serializer.data, expected) + serializer.save() self.assertEquals(serializer.data, data) From f62acf025e938d14b1e74d3cecb4f42975793e9a Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Tue, 1 Jan 2013 18:06:22 +0100 Subject: [PATCH 129/129] Update release notes --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c93eebac8..5b34bf3de 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -16,6 +16,10 @@ Major version numbers (x.0.0) are reserved for project milestones. No major poi ## 2.1.x series +### Master + +* Relation changes are no longer persisted in `.restore_object` + ### 2.1.14 **Date**: 31st Dec 2012