From 213981cef394c6f7603c24b9a51096ffb56f6024 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 4 Jan 2013 21:11:03 +0100 Subject: Handle ObjectDoesNotExist exceptions when serializing null reverse one-to-one --- rest_framework/relations.py | 10 ++++++++-- rest_framework/serializers.py | 17 ++++++++++------- rest_framework/tests/models.py | 5 +++++ rest_framework/tests/relations_nested.py | 22 +++++++++++++++++++++- 4 files changed, 44 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 686dcf04..6c1d4f5b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -100,7 +100,10 @@ class RelatedField(WritableField): ### Regular serializer stuff... def field_to_native(self, obj, field_name): - value = getattr(obj, self.source or field_name) + try: + value = getattr(obj, self.source or field_name) + except ObjectDoesNotExist: + return None return self.to_native(value) def field_from_native(self, data, files, field_name, into): @@ -202,7 +205,10 @@ class PrimaryKeyRelatedField(RelatedField): pk = obj.serializable_value(self.source or field_name) except AttributeError: # RelatedObject (reverse relationship) - obj = getattr(obj, self.source or field_name) + try: + obj = getattr(obj, self.source or field_name) + except ObjectDoesNotExist: + return None return self.to_native(obj.pk) # Forward relationship return self.to_native(pk) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bd54db4c..3391a262 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -293,15 +293,18 @@ class BaseSerializer(Field): Override default so that we can apply ModelSerializer as a nested field to relationships. """ - if self.source: - for component in self.source.split('.'): - obj = getattr(obj, component) + try: + if self.source: + for component in self.source.split('.'): + obj = getattr(obj, component) + if is_simple_callable(obj): + obj = obj() + else: + obj = getattr(obj, field_name) if is_simple_callable(obj): obj = obj() - else: - obj = getattr(obj, field_name) - if is_simple_callable(obj): - obj = value() + except ObjectDoesNotExist: + return None # 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 59c35074..34cdbff3 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -205,3 +205,8 @@ class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, related_name='nullable_sources') + +class NullableOneToOneSource(RESTFrameworkModel): + name = models.CharField(max_length=100) + target = models.OneToOneField(ForeignKeyTarget, null=True, blank=True, + related_name='nullable_source') diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 5710c1ef..808399f7 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -1,7 +1,7 @@ from django.db import models from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource +from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, NullableOneToOneSource class ForeignKeySourceSerializer(serializers.ModelSerializer): @@ -28,6 +28,13 @@ class NullableForeignKeySourceSerializer(serializers.ModelSerializer): model = NullableForeignKeySource +class NullableForeignKeyTargetSerializer(serializers.ModelSerializer): + nullable_source = serializers.PrimaryKeyRelatedField() + + class Meta: + model = ForeignKeyTarget + + class ReverseForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') @@ -67,6 +74,10 @@ class NestedNullableForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') target.save() + new_target = ForeignKeyTarget(name='target-2') + new_target.save() + one_source = NullableOneToOneSource(name='one-source-1', target=target) + one_source.save() for idx in range(1, 4): if idx == 3: target = None @@ -82,3 +93,12 @@ class NestedNullableForeignKeyTests(TestCase): {'id': 3, 'name': u'source-3', 'target': None}, ] self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_retrieve_with_null(self): + queryset = ForeignKeyTarget.objects.all() + serializer = NullableForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'nullable_source': 1}, + {'id': 2, 'name': u'target-2', 'nullable_source': None}, + ] + self.assertEquals(serializer.data, expected) -- cgit v1.2.3 From 4bb504732d045fb7db0fc1ddc1fc926197dd2c91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Jan 2013 21:08:55 +0000 Subject: Respect blank=True on relational fields. Fixes #537 --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index fa92838b..19a955b8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -430,7 +430,7 @@ class ModelSerializer(Serializer): # TODO: filter queryset using: # .using(db).complex_filter(self.rel.limit_choices_to) kwargs = { - 'null': model_field.null, + 'null': model_field.null or model_field.blank, 'queryset': model_field.rel.to._default_manager } -- cgit v1.2.3 From 4e8f55887d6ce86a2293f8b8cbb255bc58995336 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Jan 2013 21:37:44 +0000 Subject: Clean up test slightly. Refs #552 --- rest_framework/tests/pagination.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 81d297a1..3b550877 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -181,10 +181,10 @@ class UnitTestPagination(TestCase): """ Ensure context gets passed through to the object serializer. """ - serializer = PassOnContextPaginationSerializer(self.first_page) + serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'}) serializer.data results = serializer.fields[serializer.results_field] - self.assertTrue(serializer.context is results.context) + self.assertEquals(serializer.context, results.context) class TestUnpaginated(TestCase): -- cgit v1.2.3 From a897eb5480348838b11fdb428ce0d110e8bc8da1 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Mon, 7 Jan 2013 16:27:31 -0800 Subject: Create separate *NullableOneToOneTests TestCase --- rest_framework/tests/models.py | 8 +++++- rest_framework/tests/relations_hyperlink.py | 38 +++++++++++++++++++++++------ rest_framework/tests/relations_nested.py | 33 ++++++++++++++++--------- rest_framework/tests/relations_pk.py | 29 +++++++++++++++++++++- 4 files changed, 88 insertions(+), 20 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 34cdbff3..93f09761 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -206,7 +206,13 @@ class NullableForeignKeySource(RESTFrameworkModel): target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, related_name='nullable_sources') + +# OneToOne +class OneToOneTarget(RESTFrameworkModel): + name = models.CharField(max_length=100) + + class NullableOneToOneSource(RESTFrameworkModel): name = models.CharField(max_length=100) - target = models.OneToOneField(ForeignKeyTarget, null=True, blank=True, + target = models.OneToOneField(OneToOneTarget, null=True, blank=True, related_name='nullable_source') diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index a7f8a035..ef57dc83 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -2,7 +2,7 @@ from django.db import models from django.test import TestCase from rest_framework import serializers from rest_framework.compat import patterns, url -from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource +from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource def dummy_view(request, pk): pass @@ -13,6 +13,8 @@ urlpatterns = patterns('', 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'), + url(r'^onetoonetarget/(?P[0-9]+)/$', dummy_view, name='onetoonetarget-detail'), + url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'), ) class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer): @@ -40,16 +42,17 @@ class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): # Nullable ForeignKey +class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = NullableForeignKeySource -class NullableForeignKeySource(models.Model): - name = models.CharField(max_length=100) - target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, - related_name='nullable_sources') +# OneToOne +class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer): + nullable_source = serializers.HyperlinkedRelatedField(view_name='nullableonetoonesource-detail') -class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): class Meta: - model = NullableForeignKeySource + model = OneToOneTarget # TODO: Add test that .data cannot be accessed prior to .is_valid @@ -409,3 +412,24 @@ class HyperlinkedNullableForeignKeyTests(TestCase): # {'id': 2, 'name': u'target-2', 'sources': []}, # ] # self.assertEquals(serializer.data, expected) + + +class HyperlinkedNullableOneToOneTests(TestCase): + urls = 'rest_framework.tests.relations_hyperlink' + + def setUp(self): + target = OneToOneTarget(name='target-1') + target.save() + new_target = OneToOneTarget(name='target-2') + new_target.save() + source = NullableOneToOneSource(name='source-1', target=target) + source.save() + + def test_reverse_foreign_key_retrieve_with_null(self): + queryset = OneToOneTarget.objects.all() + serializer = NullableOneToOneTargetSerializer(queryset) + expected = [ + {'url': '/onetoonetarget/1/', 'name': u'target-1', 'nullable_source': '/nullableonetoonesource/1/'}, + {'url': '/onetoonetarget/2/', 'name': u'target-2', 'nullable_source': None}, + ] + self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 808399f7..225fee88 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -1,7 +1,7 @@ from django.db import models from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, NullableOneToOneSource +from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource class ForeignKeySourceSerializer(serializers.ModelSerializer): @@ -28,11 +28,16 @@ class NullableForeignKeySourceSerializer(serializers.ModelSerializer): model = NullableForeignKeySource -class NullableForeignKeyTargetSerializer(serializers.ModelSerializer): - nullable_source = serializers.PrimaryKeyRelatedField() +class NullableOneToOneSourceSerializer(serializers.ModelSerializer): + class Meta: + model = NullableOneToOneSource + + +class NullableOneToOneTargetSerializer(serializers.ModelSerializer): + nullable_source = NullableOneToOneSourceSerializer() class Meta: - model = ForeignKeyTarget + model = OneToOneTarget class ReverseForeignKeyTests(TestCase): @@ -74,10 +79,6 @@ class NestedNullableForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') target.save() - new_target = ForeignKeyTarget(name='target-2') - new_target.save() - one_source = NullableOneToOneSource(name='one-source-1', target=target) - one_source.save() for idx in range(1, 4): if idx == 3: target = None @@ -94,11 +95,21 @@ class NestedNullableForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + +class NestedNullableOneToOneTests(TestCase): + def setUp(self): + target = OneToOneTarget(name='target-1') + target.save() + new_target = OneToOneTarget(name='target-2') + new_target.save() + source = NullableOneToOneSource(name='source-1', target=target) + source.save() + def test_reverse_foreign_key_retrieve_with_null(self): - queryset = ForeignKeyTarget.objects.all() - serializer = NullableForeignKeyTargetSerializer(queryset) + queryset = OneToOneTarget.objects.all() + serializer = NullableOneToOneTargetSerializer(queryset) expected = [ - {'id': 1, 'name': u'target-1', 'nullable_source': 1}, + {'id': 1, 'name': u'target-1', 'nullable_source': {'id': 1, 'name': u'source-1', 'target': 1}}, {'id': 2, 'name': u'target-2', 'nullable_source': None}, ] self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index af6da2c0..589b3646 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,7 +1,7 @@ from django.db import models from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource +from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource class ManyToManyTargetSerializer(serializers.ModelSerializer): @@ -33,6 +33,14 @@ class NullableForeignKeySourceSerializer(serializers.ModelSerializer): model = NullableForeignKeySource +# OneToOne +class NullableOneToOneTargetSerializer(serializers.ModelSerializer): + nullable_source = serializers.PrimaryKeyRelatedField() + + class Meta: + model = OneToOneTarget + + # TODO: Add test that .data cannot be accessed prior to .is_valid class PKManyToManyTests(TestCase): @@ -383,3 +391,22 @@ class PKNullableForeignKeyTests(TestCase): # {'id': 2, 'name': u'target-2', 'sources': []}, # ] # self.assertEquals(serializer.data, expected) + + +class PKNullableOneToOneTests(TestCase): + def setUp(self): + target = OneToOneTarget(name='target-1') + target.save() + new_target = OneToOneTarget(name='target-2') + new_target.save() + source = NullableOneToOneSource(name='source-1', target=target) + source.save() + + def test_reverse_foreign_key_retrieve_with_null(self): + queryset = OneToOneTarget.objects.all() + serializer = NullableOneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'nullable_source': 1}, + {'id': 2, 'name': u'target-2', 'nullable_source': None}, + ] + self.assertEquals(serializer.data, expected) -- cgit v1.2.3 From 49cd5e59a8679004a067e2db72f8ba1d9ac5b69c Mon Sep 17 00:00:00 2001 From: Marc Tamlyn Date: Tue, 8 Jan 2013 12:20:01 +0000 Subject: ObtainAuthToken pluggable Serializer. It should have serializer_class in the same way as any other API view.--- rest_framework/authtoken/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index d318c723..7c03cb76 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -12,10 +12,11 @@ class ObtainAuthToken(APIView): permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) renderer_classes = (renderers.JSONRenderer,) + serializer_class = AuthTokenSerializer model = Token def post(self, request): - serializer = AuthTokenSerializer(data=request.DATA) + serializer = self.serializer_class(data=request.DATA) if serializer.is_valid(): token, created = Token.objects.get_or_create(user=serializer.object['user']) return Response({'token': token.key}) -- cgit v1.2.3 From c1f194b0a592c88c7de512958f62c43695df018f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 8 Jan 2013 15:03:14 +0000 Subject: Fix inconsistent view_name logic. Fixes #567. --- rest_framework/relations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 0d93f448..adc47800 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -367,13 +367,13 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(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) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass @@ -490,13 +490,13 @@ class HyperlinkedIdentityField(Field): kwargs = {self.slug_url_kwarg: slug} try: - return reverse(self.view_name, kwargs=kwargs, request=request, format=format) + return reverse(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) + return reverse(view_name, kwargs=kwargs, request=request, format=format) except: pass -- cgit v1.2.3 From 268f60999cdca46e6541f5acc35fbbe08b85f477 Mon Sep 17 00:00:00 2001 From: Juan Riaza Date: Thu, 10 Jan 2013 15:48:22 +0100 Subject: unused imports --- rest_framework/tests/decorators.py | 1 - rest_framework/tests/relations_hyperlink.py | 1 - rest_framework/tests/relations_nested.py | 1 - rest_framework/tests/relations_pk.py | 1 - 4 files changed, 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index bc44a45b..5e6bce4e 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,5 +1,4 @@ from django.test import TestCase -from django.test.client import RequestFactory from rest_framework import status from rest_framework.response import Response from rest_framework.renderers import JSONRenderer diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index ef57dc83..57913670 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -1,4 +1,3 @@ -from django.db import models from django.test import TestCase from rest_framework import serializers from rest_framework.compat import patterns, url diff --git a/rest_framework/tests/relations_nested.py b/rest_framework/tests/relations_nested.py index 225fee88..0e129fae 100644 --- a/rest_framework/tests/relations_nested.py +++ b/rest_framework/tests/relations_nested.py @@ -1,4 +1,3 @@ -from django.db import models from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 589b3646..54835860 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -1,4 +1,3 @@ -from django.db import models from django.test import TestCase from rest_framework import serializers from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource -- cgit v1.2.3 From 73c4e5c4603e24ec1ea9976a3c6152a797f8f041 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 12 Jan 2013 09:32:53 +0000 Subject: auto_now and auto_now_add fields should be read only by default --- rest_framework/serializers.py | 3 +++ rest_framework/tests/fields.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 rest_framework/tests/fields.py (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index da0af467..0bacacda 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -452,6 +452,9 @@ class ModelSerializer(Serializer): if model_field.null or model_field.blank: kwargs['required'] = False + if not model_field.editable: + kwargs['read_only'] = True + if model_field.has_default(): kwargs['required'] = False kwargs['default'] = model_field.get_default() diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py new file mode 100644 index 00000000..b1a8161a --- /dev/null +++ b/rest_framework/tests/fields.py @@ -0,0 +1,26 @@ +""" +General tests for relational fields. +""" + +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +class TimestampedModel(models.Model): + added = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + +class TimestampedModelSerializer(serializers.ModelSerializer): + class Meta: + model = TimestampedModel + + +class ReadOnlyFieldTests(TestCase): + def test_auto_now_fields_read_only(self): + """ + auto_now and auto_now_add fields should be readonly by default. + """ + serializer = TimestampedModelSerializer() + self.assertEquals(serializer.fields['added'].read_only, True) -- cgit v1.2.3 From d9acec3e6dd07d33f416646bc161108ea52842d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 12 Jan 2013 09:43:07 +0000 Subject: PK fields should only be read-only if they are an AutoField. Fixes #563 --- rest_framework/serializers.py | 5 +++-- rest_framework/tests/fields.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 0bacacda..27458f96 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -415,7 +415,7 @@ class ModelSerializer(Serializer): """ Returns a default instance of the pk field. """ - return Field() + return self.get_field(model_field) def get_nested_field(self, model_field): """ @@ -452,7 +452,7 @@ class ModelSerializer(Serializer): if model_field.null or model_field.blank: kwargs['required'] = False - if not model_field.editable: + if isinstance(model_field, models.AutoField) or not model_field.editable: kwargs['read_only'] = True if model_field.has_default(): @@ -468,6 +468,7 @@ class ModelSerializer(Serializer): return ChoiceField(**kwargs) field_mapping = { + models.AutoField: IntegerField, models.FloatField: FloatField, models.IntegerField: IntegerField, models.PositiveIntegerField: IntegerField, diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index b1a8161a..a6a05941 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -12,11 +12,20 @@ class TimestampedModel(models.Model): updated = models.DateTimeField(auto_now=True) +class CharPrimaryKeyModel(models.Model): + id = models.CharField(max_length=20, primary_key=True) + + class TimestampedModelSerializer(serializers.ModelSerializer): class Meta: model = TimestampedModel +class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): + class Meta: + model = CharPrimaryKeyModel + + class ReadOnlyFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ @@ -24,3 +33,11 @@ class ReadOnlyFieldTests(TestCase): """ serializer = TimestampedModelSerializer() self.assertEquals(serializer.fields['added'].read_only, True) + + def test_auto_pk_fields_read_only(self): + serializer = TimestampedModelSerializer() + self.assertEquals(serializer.fields['id'].read_only, True) + + def test_non_auto_pk_fields_not_read_only(self): + serializer = CharPrimaryKeyModelSerializer() + self.assertEquals(serializer.fields['id'].read_only, False) -- cgit v1.2.3 From 191135d7b06617069489fa5de4e6f519767a4ee1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 14 Jan 2013 09:20:37 +0000 Subject: Version 2.1.16 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 1d25ee7f..bc267fad 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.15' +__version__ = '2.1.16' VERSION = __version__ # synonym -- cgit v1.2.3 From da6b9576c5c5f019800555108fc86887ef7e453d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 10:51:10 +0000 Subject: Update docstrings --- rest_framework/tests/fields.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py index a6a05941..8068272d 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -1,5 +1,5 @@ """ -General tests for relational fields. +General serializer field tests. """ from django.db import models @@ -29,15 +29,21 @@ class CharPrimaryKeyModelSerializer(serializers.ModelSerializer): class ReadOnlyFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ - auto_now and auto_now_add fields should be readonly by default. + auto_now and auto_now_add fields should be read_only by default. """ serializer = TimestampedModelSerializer() self.assertEquals(serializer.fields['added'].read_only, True) def test_auto_pk_fields_read_only(self): + """ + AutoField fields should be read_only by default. + """ serializer = TimestampedModelSerializer() self.assertEquals(serializer.fields['id'].read_only, True) def test_non_auto_pk_fields_not_read_only(self): + """ + PK fields other than AutoField fields should not be read_only by default. + """ serializer = CharPrimaryKeyModelSerializer() self.assertEquals(serializer.fields['id'].read_only, False) -- cgit v1.2.3 From e67b23f1ace59f237f260075501a8fbf636b60f1 Mon Sep 17 00:00:00 2001 From: Johannes Spielmann Date: Tue, 15 Jan 2013 13:46:41 +0100 Subject: correcting template: closing tag was missing --- rest_framework/templates/rest_framework/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 42e49cb9..092bf2e4 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -112,7 +112,7 @@
{{ request.method }} {{ request.get_full_path }}
-
+
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %} {% for key, val in response.items %}{{ key }}: {{ val|urlize_quoted_links }} -- cgit v1.2.3 From 4fc3b1ba56239a1fb999f9aef99cdbcfbc9aa254 Mon Sep 17 00:00:00 2001 From: James Cleveland Date: Tue, 15 Jan 2013 13:02:53 +0000 Subject: Add timedelta encoder to the JSONEncoder class. Whilst this commit adds *encoding* of timedeltas to a string of a floating point value of the seconds, you must add your own serializer field for whatever timedelta model field you are using. This is because Django doesn't support any kind of timedelta field out-of-the-box, so you have to either implement your own or use django-timedelta. If this is the case and you want to serialise timedelta input, you will have to implement your own special field to use for the timedelta, which is not included in core as it is based on a 3rd party library. Here is an example: import datetime import timedelta from django import forms from django.core import validators from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ from rest_framework.fields import WritableField class TimedeltaField(WritableField): type_name = 'TimedeltaField' form_field_class = forms.FloatField default_error_messages = { 'invalid': _("'%s' value must be in seconds."), } def from_native(self, value): if value in validators.EMPTY_VALUES: return None try: return datetime.timedelta(seconds=float(value)) except (TypeError, ValueError): msg = self.error_messages['invalid'] % value raise ValidationError(msg) Which is based on the FloatField. This field can then be used in your serializer like this: from yourapp.fields import TimedeltaField class YourSerializer(serializers.ModelSerializer): duration = TimedeltaField() --- rest_framework/utils/encoders.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index c70b24dd..7afe100a 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -12,7 +12,7 @@ from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata class JSONEncoder(json.JSONEncoder): """ - JSONEncoder subclass that knows how to encode date/time, + JSONEncoder subclass that knows how to encode date/time/timedelta, decimal types, and generators. """ def default(self, o): @@ -34,6 +34,8 @@ class JSONEncoder(json.JSONEncoder): if o.microsecond: r = r[:12] return r + elif isinstance(o, datetime.timedelta): + return str(o.total_seconds()) elif isinstance(o, decimal.Decimal): return str(o) elif hasattr(o, '__iter__'): -- cgit v1.2.3 From 87029122c287b4a03e309a432dc9f9668efd7c0e Mon Sep 17 00:00:00 2001 From: Steven Gregory Date: Tue, 15 Jan 2013 13:49:48 -0700 Subject: Added a new file 'relations_slug.py' that tests Nullable Foreign Keys and the SlugRelatedField --- rest_framework/tests/relations_slug.py | 281 +++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 rest_framework/tests/relations_slug.py (limited to 'rest_framework') diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py new file mode 100644 index 00000000..d56f4d4a --- /dev/null +++ b/rest_framework/tests/relations_slug.py @@ -0,0 +1,281 @@ +from django.test import TestCase +from rest_framework import serializers +from rest_framework.compat import patterns, url +from rest_framework.tests.models import NullableForeignKeySource, ForeignKeyTarget + +def dummy_view(request, pk): + pass + + +# Nullable ForeignKey +class SluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name') + class Meta: + model = NullableForeignKeySource + +class NullOKSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name', null=True) + class Meta: + model = NullableForeignKeySource + +class DefaultSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name', default='N/A') + class Meta: + model = NullableForeignKeySource + +class NotRequiredSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name', required=False) + class Meta: + model = NullableForeignKeySource + + +class SluggedNullableForeignKeyTests(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_slug_foreign_key_retrieve_with_null(self): + queryset = NullableForeignKeySource.objects.all() + + default_expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': 'N/A'}, + ] + expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': None}, + ] + + serializer = DefaultSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, default_expected) + + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + serializer = SluggedNullableForeignKeySourceSerializer(queryset) + #Throws + self.assertEquals(serializer.data, expected) + + def test_slug_foreign_key_create_with_valid_null(self): + data = {'name': u'source-4', 'target': None} + default_data = {'name': u'source-4', 'target': 'N/A'} + + serializer = SluggedNullableForeignKeySourceSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + + #If attribute not required, data should match + serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + #BUG: Throws AttributeError: "NoneType object has no attribute 'name'" + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + #If default = 'N/A' then target should pass validation, and be the default ('N/A') + serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) + #BUG: test case fails + self.assertTrue(serializer.is_valid()) + #BUG: serializer.errors = {'target': [u'Value may not be null']} + #BUG: Serializer does not use default value to save object + obj = serializer.save() + #BUG: Throws AttributeError - NoneType object has no attribute 'name' + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'source-4') + + #If null = True then target should be None + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) + #BUG: test case fails + self.assertTrue(serializer.is_valid()) + #BUG: serializer.errors = {'target': [u'Value may not be null']} + #BUG: serializer does not save object (But it can because its not required) + obj = serializer.save() + #BUG: Throws AttributeError - NoneType object has no attribute 'name' + 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() + default_expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': 'N/A'}, + {'name': u'source-4', 'target': 'N/A'} + ] + expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': None}, + {'name': u'source-4', 'target': None} + ] + serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) + self.assertEquals(serializer.data, expected) + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) + self.assertEquals(serializer.data, expected) + serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) + self.assertEquals(serializer.data, default_expected) + + def test_slug_foreign_key_create_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'name': u'source-4', 'target': ''} + expected_data = {'name': u'source-4', 'target': None} + + serializer = SluggedNullableForeignKeySourceSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, expected_data) + self.assertEqual(obj.name, u'source-4') + + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, expected_data) + self.assertEqual(obj.name, u'source-4') + + serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + 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() + default_expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': 'N/A'}, + {'name': u'source-4', 'target': 'N/A'} + ] + expected = [ + {'name': u'source-1', 'target': 'target-1'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': None}, + {'name': u'source-4', 'target': None} + ] + #BUG: All serializers fail here + serializer = DefaultSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, default_expected) + serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + def test_slug_foreign_key_update_with_valid_null(self): + data = {'name': u'source-1', 'target': None} + default_data = {'name': u'source-1', 'target': 'N/A'} + instance = NullableForeignKeySource.objects.get(pk=1) + + serializer = SluggedNullableForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + serializer = DefaultSluggedNullableForeignKeySourceSerializer(instance, data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, default_data) + serializer.save() + + serializer = NullOKSluggedNullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, data) + serializer.save() + + + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(instance, data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + expected = [ + {'name': u'source-1', 'target': None}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': None}, + ] + serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + expected = [ + {'name': u'source-1', 'target': 'N/A'}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': 'N/A'}, + ] + serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + + def test_slug_foreign_key_update_with_valid_emptystring(self): + """ + The emptystring should be interpreted as null in the context + of relationships. + """ + data = {'name': u'source-1', 'target': ''} + default_data = {'name': u'source-1', 'target': 'N/A'} + expected_data = {'name': u'source-1', 'target': None} + instance = NullableForeignKeySource.objects.get(pk=1) + + serializer = SluggedNullableForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + serializer = DefaultSluggedNullableForeignKeySourceSerializer(instance, data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + self.assertEquals(serializer.data, default_data) + serializer.save() + + serializer = NullOKSluggedNullableForeignKeySourceSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, data) + serializer.save() + + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(instance, data=data) + #BUG: is_valid() is False + self.assertTrue(serializer.is_valid()) + #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' + self.assertEquals(serializer.data, data) + serializer.save() + + # Ensure source 1 is updated, and everything else is as expected + queryset = NullableForeignKeySource.objects.all() + expected = [ + {'name': u'source-1', 'target': None}, + {'name': u'source-2', 'target': 'target-1'}, + {'name': u'source-3', 'target': None}, + ] + serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) + self.assertEquals(serializer.data, expected) + -- cgit v1.2.3 From eb3d4d0e93b07d245232685d4fe3ad78144ea933 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 16 Jan 2013 14:32:51 +0000 Subject: Drop bits of relations_slug tests which don't mirror existing tests. --- rest_framework/relations.py | 3 + rest_framework/tests/relations_hyperlink.py | 2 + rest_framework/tests/relations_pk.py | 2 +- rest_framework/tests/relations_slug.py | 246 +++++----------------------- 4 files changed, 47 insertions(+), 206 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 5e4552b7..7ded3891 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -105,6 +105,9 @@ class RelatedField(WritableField): value = getattr(obj, self.source or field_name) except ObjectDoesNotExist: return None + + if value is None: + return None return self.to_native(value) def field_from_native(self, data, files, field_name, into): diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 57913670..7d65eae7 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -3,6 +3,7 @@ from rest_framework import serializers from rest_framework.compat import patterns, url from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource + def dummy_view(request, pk): pass @@ -16,6 +17,7 @@ urlpatterns = patterns('', url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'), ) + class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer): sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail') diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index 54835860..dd1e86b5 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -206,7 +206,7 @@ class PKForeignKeyTests(TestCase): 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() diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index d56f4d4a..503b61e8 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -1,36 +1,18 @@ from django.test import TestCase from rest_framework import serializers -from rest_framework.compat import patterns, url from rest_framework.tests.models import NullableForeignKeySource, ForeignKeyTarget -def dummy_view(request, pk): - pass - -# Nullable ForeignKey -class SluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): - target = serializers.SlugRelatedField(slug_field='name') - class Meta: - model = NullableForeignKeySource - -class NullOKSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): +class NullableSlugSourceSerializer(serializers.ModelSerializer): target = serializers.SlugRelatedField(slug_field='name', null=True) - class Meta: - model = NullableForeignKeySource -class DefaultSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): - target = serializers.SlugRelatedField(slug_field='name', default='N/A') class Meta: model = NullableForeignKeySource -class NotRequiredSluggedNullableForeignKeySourceSerializer(serializers.ModelSerializer): - target = serializers.SlugRelatedField(slug_field='name', required=False) - class Meta: - model = NullableForeignKeySource +# TODO: M2M Tests, FKTests (Non-nulable), One2One -class SluggedNullableForeignKeyTests(TestCase): - +class SlugNullableForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1') target.save() @@ -40,242 +22,96 @@ class SluggedNullableForeignKeyTests(TestCase): source = NullableForeignKeySource(name='source-%d' % idx, target=target) source.save() - def test_slug_foreign_key_retrieve_with_null(self): + def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - - default_expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': 'N/A'}, - ] + serializer = NullableSlugSourceSerializer(queryset) expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': None}, + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': None}, ] - - serializer = DefaultSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, default_expected) - - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, expected) - - serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) self.assertEquals(serializer.data, expected) - serializer = SluggedNullableForeignKeySourceSerializer(queryset) - #Throws - self.assertEquals(serializer.data, expected) - - def test_slug_foreign_key_create_with_valid_null(self): - data = {'name': u'source-4', 'target': None} - default_data = {'name': u'source-4', 'target': 'N/A'} - - serializer = SluggedNullableForeignKeySourceSerializer(data=data) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) - - - #If attribute not required, data should match - serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - #BUG: Throws AttributeError: "NoneType object has no attribute 'name'" - self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') - - #If default = 'N/A' then target should pass validation, and be the default ('N/A') - serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) - #BUG: test case fails + def test_foreign_key_create_with_valid_null(self): + data = {'id': 4, 'name': u'source-4', 'target': None} + serializer = NullableSlugSourceSerializer(data=data) self.assertTrue(serializer.is_valid()) - #BUG: serializer.errors = {'target': [u'Value may not be null']} - #BUG: Serializer does not use default value to save object obj = serializer.save() - #BUG: Throws AttributeError - NoneType object has no attribute 'name' - self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'source-4') - - #If null = True then target should be None - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) - #BUG: test case fails - self.assertTrue(serializer.is_valid()) - #BUG: serializer.errors = {'target': [u'Value may not be null']} - #BUG: serializer does not save object (But it can because its not required) - obj = serializer.save() - #BUG: Throws AttributeError - NoneType object has no attribute 'name' 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() - default_expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': 'N/A'}, - {'name': u'source-4', 'target': 'N/A'} - ] + serializer = NullableSlugSourceSerializer(queryset) expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': None}, - {'name': u'source-4', 'target': None} + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': None}, + {'id': 4, 'name': u'source-4', 'target': None} ] - serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) - self.assertEquals(serializer.data, expected) - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) self.assertEquals(serializer.data, expected) - serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) - self.assertEquals(serializer.data, default_expected) - def test_slug_foreign_key_create_with_valid_emptystring(self): + def test_foreign_key_create_with_valid_emptystring(self): """ The emptystring should be interpreted as null in the context of relationships. """ - data = {'name': u'source-4', 'target': ''} - expected_data = {'name': u'source-4', 'target': None} - - serializer = SluggedNullableForeignKeySourceSerializer(data=data) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) - - serializer = NullOKSluggedNullableForeignKeySourceSerializer(data=data) + data = {'id': 4, 'name': u'source-4', 'target': ''} + expected_data = {'id': 4, 'name': u'source-4', 'target': None} + serializer = NullableSlugSourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' - self.assertEquals(serializer.data, expected_data) - self.assertEqual(obj.name, u'source-4') - - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(data=data) - #BUG: is_valid() is False - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' - self.assertEquals(serializer.data, expected_data) - self.assertEqual(obj.name, u'source-4') - - serializer = DefaultSluggedNullableForeignKeySourceSerializer(data=data) - #BUG: is_valid() is False - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' 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() - default_expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': 'N/A'}, - {'name': u'source-4', 'target': 'N/A'} - ] + serializer = NullableSlugSourceSerializer(queryset) expected = [ - {'name': u'source-1', 'target': 'target-1'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': None}, - {'name': u'source-4', 'target': None} + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': None}, + {'id': 4, 'name': u'source-4', 'target': None} ] - #BUG: All serializers fail here - serializer = DefaultSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, default_expected) - serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, expected) - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) self.assertEquals(serializer.data, expected) - def test_slug_foreign_key_update_with_valid_null(self): - data = {'name': u'source-1', 'target': None} - default_data = {'name': u'source-1', 'target': 'N/A'} + 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 = SluggedNullableForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) - - serializer = DefaultSluggedNullableForeignKeySourceSerializer(instance, data=data) - #BUG: is_valid() is False + serializer = NullableSlugSourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, default_data) - serializer.save() - - serializer = NullOKSluggedNullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' - self.assertEquals(serializer.data, data) - serializer.save() - - - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(instance, data=data) - #BUG: is_valid() is False - self.assertTrue(serializer.is_valid()) - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' self.assertEquals(serializer.data, data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() + serializer = NullableSlugSourceSerializer(queryset) expected = [ - {'name': u'source-1', 'target': None}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': None}, + {'id': 1, 'name': u'source-1', 'target': None}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': None} ] - serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) self.assertEquals(serializer.data, expected) - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, expected) - - expected = [ - {'name': u'source-1', 'target': 'N/A'}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': 'N/A'}, - ] - serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, expected) - - def test_slug_foreign_key_update_with_valid_emptystring(self): + def test_foreign_key_update_with_valid_emptystring(self): """ The emptystring should be interpreted as null in the context of relationships. """ - data = {'name': u'source-1', 'target': ''} - default_data = {'name': u'source-1', 'target': 'N/A'} - expected_data = {'name': u'source-1', 'target': None} + 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 = SluggedNullableForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) - - serializer = DefaultSluggedNullableForeignKeySourceSerializer(instance, data=data) - #BUG: is_valid() is False - self.assertTrue(serializer.is_valid()) - self.assertEquals(serializer.data, default_data) - serializer.save() - - serializer = NullOKSluggedNullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' - self.assertEquals(serializer.data, data) - serializer.save() - - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(instance, data=data) - #BUG: is_valid() is False + serializer = NullableSlugSourceSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) - #BUG: Throws AttributeError: 'NoneType' object has no attribute 'name' - self.assertEquals(serializer.data, data) + self.assertEquals(serializer.data, expected_data) serializer.save() # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() + serializer = NullableSlugSourceSerializer(queryset) expected = [ - {'name': u'source-1', 'target': None}, - {'name': u'source-2', 'target': 'target-1'}, - {'name': u'source-3', 'target': None}, + {'id': 1, 'name': u'source-1', 'target': None}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': None} ] - serializer = NullOKSluggedNullableForeignKeySourceSerializer(queryset) self.assertEquals(serializer.data, expected) - serializer = NotRequiredSluggedNullableForeignKeySourceSerializer(queryset) - self.assertEquals(serializer.data, expected) - -- cgit v1.2.3 From 72c04d570d167209f3f34d6d78492426f206b245 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 4 Jan 2013 12:50:01 +0100 Subject: Add nested create for 1to1 reverse relationships --- rest_framework/serializers.py | 46 ++++++++++++++++++---- rest_framework/tests/nesting.py | 85 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 rest_framework/tests/nesting.py (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 27458f96..a43a81d7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -93,7 +93,7 @@ class SerializerOptions(object): self.exclude = getattr(meta, 'exclude', ()) -class BaseSerializer(Field): +class BaseSerializer(WritableField): class Meta(object): pass @@ -218,7 +218,10 @@ class BaseSerializer(Field): try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: - self._errors[field_name] = list(err.messages) + if hasattr(err, 'message_dict'): + self._errors[field_name] = [err.message_dict] + else: + self._errors[field_name] = list(err.messages) return reverted_data @@ -369,6 +372,25 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions + def field_from_native(self, data, files, field_name, into): + if self.read_only: + return + + try: + native = data[field_name] + except KeyError: + if self.required: + raise ValidationError(self.error_messages['required']) + return + + obj = self.from_native(native, files) + if not self._errors: + self.object = obj + into[self.source or field_name] = self + else: + # Propagate errors up to our parent + raise ValidationError(self._errors) + def get_default_fields(self): """ Return all the fields that should be serialized for the model. @@ -542,10 +564,9 @@ class ModelSerializer(Serializer): return instance - def save(self): - """ - Save the deserialized object and return it. - """ + def _save(self, parent=None, fk_field=None): + if parent and fk_field: + setattr(self.object, fk_field, parent) self.object.save() if getattr(self, 'm2m_data', None): @@ -555,9 +576,18 @@ class ModelSerializer(Serializer): if getattr(self, 'related_data', None): for accessor_name, object_list in self.related_data.items(): - setattr(self.object, accessor_name, object_list) + if isinstance(object_list, ModelSerializer): + fk_field = self.object._meta.get_field_by_name(accessor_name)[0].field.name + object_list._save(parent=self.object, fk_field=fk_field) + else: + setattr(self.object, accessor_name, object_list) self.related_data = {} - + + def save(self): + """ + Save the deserialized object and return it. + """ + self._save() return self.object diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py new file mode 100644 index 00000000..0c130dce --- /dev/null +++ b/rest_framework/tests/nesting.py @@ -0,0 +1,85 @@ +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +class OneToOneTarget(models.Model): + name = models.CharField(max_length=100) + + +class OneToOneTargetSource(models.Model): + name = models.CharField(max_length=100) + target = models.OneToOneField(OneToOneTarget, related_name='target_source') + + +class OneToOneSource(models.Model): + name = models.CharField(max_length=100) + target_source = models.OneToOneField(OneToOneTargetSource, related_name='source') + + +class OneToOneSourceSerializer(serializers.ModelSerializer): + class Meta: + model = OneToOneSource + exclude = ('target_source', ) + + +class OneToOneTargetSourceSerializer(serializers.ModelSerializer): + source = OneToOneSourceSerializer() + + class Meta: + model = OneToOneTargetSource + exclude = ('target', ) + +class OneToOneTargetSerializer(serializers.ModelSerializer): + target_source = OneToOneTargetSourceSerializer() + + class Meta: + model = OneToOneTarget + + +class NestedOneToOneTests(TestCase): + def setUp(self): + #import pdb ; pdb.set_trace() + for idx in range(1, 4): + target = OneToOneTarget(name='target-%d' % idx) + target.save() + target_source = OneToOneTargetSource(name='target-source-%d' % idx, target=target) + target_source.save() + source = OneToOneSource(name='source-%d' % idx, target_source=target_source) + source.save() + + def test_foreign_key_retrieve(self): + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, + {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, + {'id': 3, 'name': u'target-3', 'target_source': {'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}} + ] + self.assertEquals(serializer.data, expected) + + + def test_foreign_key_create(self): + data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4, 'name': u'source-4'}}} + serializer = OneToOneTargetSerializer(data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'target-4') + + # Ensure (source 4, target 4) is added, and everything else is as expected + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, + {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, + {'id': 3, 'name': u'target-3', 'target_source': {'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}}, + {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4, 'name': u'source-4'}}} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_create_with_invalid_data(self): + data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4}}} + serializer = OneToOneTargetSerializer(data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target_source': [{'source': [{'name': [u'This field is required.']}]}]}) -- cgit v1.2.3 From e66eeb4af8611ba255274f561afb674b25a93c8a Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 4 Jan 2013 13:13:03 +0100 Subject: Remove commented out debug code --- rest_framework/tests/nesting.py | 1 - 1 file changed, 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index 0c130dce..d6f9237f 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -39,7 +39,6 @@ class OneToOneTargetSerializer(serializers.ModelSerializer): class NestedOneToOneTests(TestCase): def setUp(self): - #import pdb ; pdb.set_trace() for idx in range(1, 4): target = OneToOneTarget(name='target-%d' % idx) target.save() -- cgit v1.2.3 From 46eea97380ab9723d747b41fab0a305dec19c738 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 4 Jan 2013 13:48:01 +0100 Subject: Update one-to-one test names --- rest_framework/tests/nesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index d6f9237f..9cc46c6c 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -47,7 +47,7 @@ class NestedOneToOneTests(TestCase): source = OneToOneSource(name='source-%d' % idx, target_source=target_source) source.save() - def test_foreign_key_retrieve(self): + def test_one_to_one_retrieve(self): queryset = OneToOneTarget.objects.all() serializer = OneToOneTargetSerializer(queryset) expected = [ @@ -58,7 +58,7 @@ class NestedOneToOneTests(TestCase): self.assertEquals(serializer.data, expected) - def test_foreign_key_create(self): + def test_one_to_one_create(self): data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4, 'name': u'source-4'}}} serializer = OneToOneTargetSerializer(data=data) self.assertTrue(serializer.is_valid()) @@ -77,7 +77,7 @@ class NestedOneToOneTests(TestCase): ] self.assertEquals(serializer.data, expected) - def test_foreign_key_create_with_invalid_data(self): + def test_one_to_one_create_with_invalid_data(self): data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4}}} serializer = OneToOneTargetSerializer(data=data) self.assertFalse(serializer.is_valid()) -- cgit v1.2.3 From 8e5003a1f6e61664e99a376ef8c200f53c4507e1 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Fri, 4 Jan 2013 13:54:51 +0100 Subject: Update errant test comment --- rest_framework/tests/nesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index 9cc46c6c..dbc8ebc9 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -66,7 +66,8 @@ class NestedOneToOneTests(TestCase): self.assertEquals(serializer.data, data) self.assertEqual(obj.name, u'target-4') - # Ensure (source 4, target 4) is added, and everything else is as expected + # Ensure (target 4, target_source 4, source 4) are added, and + # everything else is as expected. queryset = OneToOneTarget.objects.all() serializer = OneToOneTargetSerializer(queryset) expected = [ -- cgit v1.2.3 From 2d62bcd5aaa6d8f25f22b3e6b89ce26c44d9dfc4 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Sat, 5 Jan 2013 00:02:48 +0100 Subject: Add one-to-one nested update and delete functionality --- rest_framework/serializers.py | 14 ++++++++++++++ rest_framework/tests/nesting.py | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a43a81d7..42218e7d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -107,6 +107,7 @@ class BaseSerializer(WritableField): self.parent = None self.root = None self.partial = partial + self.delete = False self.context = context or {} @@ -215,6 +216,15 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) + if isinstance(field, ModelSerializer) and self.object: + # Set the serializer object if it exists + pk_field_name = field.opts.model._meta.pk.name + obj = getattr(self.object, field_name) + nested_data = data.get(field_name) + pk_val = nested_data.get(pk_field_name) if nested_data else None + if obj and (getattr(obj, pk_field_name) == pk_val): + field.object = obj + field.delete = nested_data.get('_delete') try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: @@ -565,6 +575,10 @@ class ModelSerializer(Serializer): return instance def _save(self, parent=None, fk_field=None): + if self.delete: + self.object.delete() + return + if parent and fk_field: setattr(self.object, fk_field, parent) self.object.save() diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index dbc8ebc9..10d5db99 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -9,7 +9,8 @@ class OneToOneTarget(models.Model): class OneToOneTargetSource(models.Model): name = models.CharField(max_length=100) - target = models.OneToOneField(OneToOneTarget, related_name='target_source') + target = models.OneToOneField(OneToOneTarget, null=True, blank=True, + related_name='target_source') class OneToOneSource(models.Model): @@ -83,3 +84,41 @@ class NestedOneToOneTests(TestCase): serializer = OneToOneTargetSerializer(data=data) self.assertFalse(serializer.is_valid()) self.assertEquals(serializer.errors, {'target_source': [{'source': [{'name': [u'This field is required.']}]}]}) + + def test_one_to_one_update(self): + data = {'id': 3, 'name': u'target-3-updated', 'target_source': {'id': 3, 'name': u'target-source-3-updated', 'source': {'id': 3, 'name': u'source-3-updated'}}} + instance = OneToOneTarget.objects.get(pk=3) + serializer = OneToOneTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + self.assertEquals(serializer.data, data) + self.assertEqual(obj.name, u'target-3-updated') + + # Ensure (target 3, target_source 3, source 3) are updated, + # and everything else is as expected. + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, + {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, + {'id': 3, 'name': u'target-3-updated', 'target_source': {'id': 3, 'name': u'target-source-3-updated', 'source': {'id': 3, 'name': u'source-3-updated'}}} + ] + self.assertEquals(serializer.data, expected) + + def test_one_to_one_delete(self): + data = {'id': 3, 'name': u'target-3', 'target_source': {'_delete': True, 'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}} + instance = OneToOneTarget.objects.get(pk=3) + serializer = OneToOneTargetSerializer(instance, data=data) + self.assertTrue(serializer.is_valid()) + obj = serializer.save() + + # Ensure (target_source 3, source 3) are deleted, + # and everything else is as expected. + queryset = OneToOneTarget.objects.all() + serializer = OneToOneTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, + {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, + {'id': 3, 'name': u'target-3', 'target_source': None} + ] + self.assertEquals(serializer.data, expected) -- cgit v1.2.3 From 34e14b01e402a2b2bcaf57aab76397757e260fd6 Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Tue, 8 Jan 2013 09:38:15 -0800 Subject: Move nested serializer logic into .field_from_native() --- rest_framework/serializers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 42218e7d..83bf1bc3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -216,15 +216,6 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) - if isinstance(field, ModelSerializer) and self.object: - # Set the serializer object if it exists - pk_field_name = field.opts.model._meta.pk.name - obj = getattr(self.object, field_name) - nested_data = data.get(field_name) - pk_val = nested_data.get(pk_field_name) if nested_data else None - if obj and (getattr(obj, pk_field_name) == pk_val): - field.object = obj - field.delete = nested_data.get('_delete') try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: @@ -393,6 +384,15 @@ class ModelSerializer(Serializer): raise ValidationError(self.error_messages['required']) return + if self.parent.object: + # Set the serializer object if it exists + pk_field_name = self.opts.model._meta.pk.name + pk_val = native.get(pk_field_name) + obj = getattr(self.parent.object, field_name) + if obj and (getattr(obj, pk_field_name) == pk_val): + self.object = obj + self.delete = native.get('_delete') + obj = self.from_native(native, files) if not self._errors: self.object = obj -- cgit v1.2.3 From 221f7326c7db7b6fa1a9ba2f0181ac075e3b482c Mon Sep 17 00:00:00 2001 From: Mark Aaron Shirley Date: Wed, 16 Jan 2013 16:03:59 -0800 Subject: Use None to delete nested object as opposed to _delete flag --- rest_framework/serializers.py | 27 ++++++++++++++------------- rest_framework/tests/nesting.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 83bf1bc3..a84370e9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -107,7 +107,6 @@ class BaseSerializer(WritableField): self.parent = None self.root = None self.partial = partial - self.delete = False self.context = context or {} @@ -119,6 +118,7 @@ class BaseSerializer(WritableField): self._data = None self._files = None self._errors = None + self._delete = False ##### # Methods to determine which fields to use when (de)serializing objects. @@ -378,7 +378,7 @@ class ModelSerializer(Serializer): return try: - native = data[field_name] + value = data[field_name] except KeyError: if self.required: raise ValidationError(self.error_messages['required']) @@ -387,19 +387,20 @@ class ModelSerializer(Serializer): if self.parent.object: # Set the serializer object if it exists pk_field_name = self.opts.model._meta.pk.name - pk_val = native.get(pk_field_name) obj = getattr(self.parent.object, field_name) - if obj and (getattr(obj, pk_field_name) == pk_val): - self.object = obj - self.delete = native.get('_delete') - - obj = self.from_native(native, files) - if not self._errors: self.object = obj - into[self.source or field_name] = self + + if value in (None, ''): + self._delete = True + into[(self.source or field_name)] = self else: - # Propagate errors up to our parent - raise ValidationError(self._errors) + obj = self.from_native(value, files) + if not self._errors: + self.object = obj + into[self.source or field_name] = self + else: + # Propagate errors up to our parent + raise ValidationError(self._errors) def get_default_fields(self): """ @@ -575,7 +576,7 @@ class ModelSerializer(Serializer): return instance def _save(self, parent=None, fk_field=None): - if self.delete: + if self._delete: self.object.delete() return diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py index 10d5db99..e4e32667 100644 --- a/rest_framework/tests/nesting.py +++ b/rest_framework/tests/nesting.py @@ -106,7 +106,7 @@ class NestedOneToOneTests(TestCase): self.assertEquals(serializer.data, expected) def test_one_to_one_delete(self): - data = {'id': 3, 'name': u'target-3', 'target_source': {'_delete': True, 'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}} + data = {'id': 3, 'name': u'target-3', 'target_source': None} instance = OneToOneTarget.objects.get(pk=3) serializer = OneToOneTargetSerializer(instance, data=data) self.assertTrue(serializer.is_valid()) -- cgit v1.2.3 From 6385ac519defc8e434fd4e24a48a680845341cb7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Jan 2013 19:47:57 +0000 Subject: Revert accidental merge. --- rest_framework/serializers.py | 61 +++----------------- rest_framework/tests/nesting.py | 124 ---------------------------------------- 2 files changed, 8 insertions(+), 177 deletions(-) delete mode 100644 rest_framework/tests/nesting.py (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a84370e9..27458f96 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -93,7 +93,7 @@ class SerializerOptions(object): self.exclude = getattr(meta, 'exclude', ()) -class BaseSerializer(WritableField): +class BaseSerializer(Field): class Meta(object): pass @@ -118,7 +118,6 @@ class BaseSerializer(WritableField): self._data = None self._files = None self._errors = None - self._delete = False ##### # Methods to determine which fields to use when (de)serializing objects. @@ -219,10 +218,7 @@ class BaseSerializer(WritableField): try: field.field_from_native(data, files, field_name, reverted_data) except ValidationError as err: - if hasattr(err, 'message_dict'): - self._errors[field_name] = [err.message_dict] - else: - self._errors[field_name] = list(err.messages) + self._errors[field_name] = list(err.messages) return reverted_data @@ -373,35 +369,6 @@ class ModelSerializer(Serializer): """ _options_class = ModelSerializerOptions - 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 self.parent.object: - # Set the serializer object if it exists - pk_field_name = self.opts.model._meta.pk.name - obj = getattr(self.parent.object, field_name) - self.object = obj - - if value in (None, ''): - self._delete = True - into[(self.source or field_name)] = self - else: - obj = self.from_native(value, files) - if not self._errors: - self.object = obj - into[self.source or field_name] = self - else: - # Propagate errors up to our parent - raise ValidationError(self._errors) - def get_default_fields(self): """ Return all the fields that should be serialized for the model. @@ -575,13 +542,10 @@ class ModelSerializer(Serializer): return instance - def _save(self, parent=None, fk_field=None): - if self._delete: - self.object.delete() - return - - if parent and fk_field: - setattr(self.object, fk_field, parent) + def save(self): + """ + Save the deserialized object and return it. + """ self.object.save() if getattr(self, 'm2m_data', None): @@ -591,18 +555,9 @@ class ModelSerializer(Serializer): if getattr(self, 'related_data', None): for accessor_name, object_list in self.related_data.items(): - if isinstance(object_list, ModelSerializer): - fk_field = self.object._meta.get_field_by_name(accessor_name)[0].field.name - object_list._save(parent=self.object, fk_field=fk_field) - else: - setattr(self.object, accessor_name, object_list) + setattr(self.object, accessor_name, object_list) self.related_data = {} - - def save(self): - """ - Save the deserialized object and return it. - """ - self._save() + return self.object diff --git a/rest_framework/tests/nesting.py b/rest_framework/tests/nesting.py deleted file mode 100644 index e4e32667..00000000 --- a/rest_framework/tests/nesting.py +++ /dev/null @@ -1,124 +0,0 @@ -from django.db import models -from django.test import TestCase -from rest_framework import serializers - - -class OneToOneTarget(models.Model): - name = models.CharField(max_length=100) - - -class OneToOneTargetSource(models.Model): - name = models.CharField(max_length=100) - target = models.OneToOneField(OneToOneTarget, null=True, blank=True, - related_name='target_source') - - -class OneToOneSource(models.Model): - name = models.CharField(max_length=100) - target_source = models.OneToOneField(OneToOneTargetSource, related_name='source') - - -class OneToOneSourceSerializer(serializers.ModelSerializer): - class Meta: - model = OneToOneSource - exclude = ('target_source', ) - - -class OneToOneTargetSourceSerializer(serializers.ModelSerializer): - source = OneToOneSourceSerializer() - - class Meta: - model = OneToOneTargetSource - exclude = ('target', ) - -class OneToOneTargetSerializer(serializers.ModelSerializer): - target_source = OneToOneTargetSourceSerializer() - - class Meta: - model = OneToOneTarget - - -class NestedOneToOneTests(TestCase): - def setUp(self): - for idx in range(1, 4): - target = OneToOneTarget(name='target-%d' % idx) - target.save() - target_source = OneToOneTargetSource(name='target-source-%d' % idx, target=target) - target_source.save() - source = OneToOneSource(name='source-%d' % idx, target_source=target_source) - source.save() - - def test_one_to_one_retrieve(self): - queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, - {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, - {'id': 3, 'name': u'target-3', 'target_source': {'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}} - ] - self.assertEquals(serializer.data, expected) - - - def test_one_to_one_create(self): - data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4, 'name': u'source-4'}}} - serializer = OneToOneTargetSerializer(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, target_source 4, source 4) are added, and - # everything else is as expected. - queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, - {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, - {'id': 3, 'name': u'target-3', 'target_source': {'id': 3, 'name': u'target-source-3', 'source': {'id': 3, 'name': u'source-3'}}}, - {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4, 'name': u'source-4'}}} - ] - self.assertEquals(serializer.data, expected) - - def test_one_to_one_create_with_invalid_data(self): - data = {'id': 4, 'name': u'target-4', 'target_source': {'id': 4, 'name': u'target-source-4', 'source': {'id': 4}}} - serializer = OneToOneTargetSerializer(data=data) - self.assertFalse(serializer.is_valid()) - self.assertEquals(serializer.errors, {'target_source': [{'source': [{'name': [u'This field is required.']}]}]}) - - def test_one_to_one_update(self): - data = {'id': 3, 'name': u'target-3-updated', 'target_source': {'id': 3, 'name': u'target-source-3-updated', 'source': {'id': 3, 'name': u'source-3-updated'}}} - instance = OneToOneTarget.objects.get(pk=3) - serializer = OneToOneTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEquals(serializer.data, data) - self.assertEqual(obj.name, u'target-3-updated') - - # Ensure (target 3, target_source 3, source 3) are updated, - # and everything else is as expected. - queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, - {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, - {'id': 3, 'name': u'target-3-updated', 'target_source': {'id': 3, 'name': u'target-source-3-updated', 'source': {'id': 3, 'name': u'source-3-updated'}}} - ] - self.assertEquals(serializer.data, expected) - - def test_one_to_one_delete(self): - data = {'id': 3, 'name': u'target-3', 'target_source': None} - instance = OneToOneTarget.objects.get(pk=3) - serializer = OneToOneTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - - # Ensure (target_source 3, source 3) are deleted, - # and everything else is as expected. - queryset = OneToOneTarget.objects.all() - serializer = OneToOneTargetSerializer(queryset) - expected = [ - {'id': 1, 'name': u'target-1', 'target_source': {'id': 1, 'name': u'target-source-1', 'source': {'id': 1, 'name': u'source-1'}}}, - {'id': 2, 'name': u'target-2', 'target_source': {'id': 2, 'name': u'target-source-2', 'source': {'id': 2, 'name': u'source-2'}}}, - {'id': 3, 'name': u'target-3', 'target_source': None} - ] - self.assertEquals(serializer.data, expected) -- cgit v1.2.3 From 211bb89eecfadd6831a0c59852926f16ea6bf733 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Jan 2013 21:29:21 +0000 Subject: Raise Validation Errors when relationships receive incorrect types. Fixes #590. --- rest_framework/relations.py | 20 ++-- rest_framework/tests/relations_hyperlink.py | 9 +- rest_framework/tests/relations_pk.py | 7 ++ rest_framework/tests/relations_slug.py | 162 ++++++++++++++++++++++++++-- 4 files changed, 177 insertions(+), 21 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7ded3891..af63ceaa 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -177,7 +177,7 @@ class PrimaryKeyRelatedField(RelatedField): default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), } # TODO: Remove these field hacks... @@ -208,7 +208,8 @@ class PrimaryKeyRelatedField(RelatedField): msg = self.error_messages['does_not_exist'] % smart_unicode(data) raise ValidationError(msg) except (TypeError, ValueError): - msg = self.error_messages['invalid'] + received = type(data).__name__ + msg = self.error_messages['incorrect_type'] % received raise ValidationError(msg) def field_to_native(self, obj, field_name): @@ -235,7 +236,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): default_error_messages = { 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), } def prepare_value(self, obj): @@ -275,7 +276,8 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): msg = self.error_messages['does_not_exist'] % smart_unicode(data) raise ValidationError(msg) except (TypeError, ValueError): - msg = self.error_messages['invalid'] + received = type(data).__name__ + msg = self.error_messages['incorrect_type'] % received raise ValidationError(msg) ### Slug relationships @@ -333,7 +335,7 @@ class HyperlinkedRelatedField(RelatedField): 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'), 'configuration_error': _('Invalid hyperlink due to configuration error'), 'does_not_exist': _("Invalid hyperlink - object does not exist."), - 'invalid': _('Invalid value.'), + 'incorrect_type': _('Incorrect type. Expected url string, received %s.'), } def __init__(self, *args, **kwargs): @@ -397,8 +399,8 @@ class HyperlinkedRelatedField(RelatedField): try: http_prefix = value.startswith('http:') or value.startswith('https:') except AttributeError: - msg = self.error_messages['invalid'] - raise ValidationError(msg) + msg = self.error_messages['incorrect_type'] + raise ValidationError(msg % type(value).__name__) if http_prefix: # If needed convert absolute URLs to relative path @@ -434,8 +436,8 @@ class HyperlinkedRelatedField(RelatedField): except ObjectDoesNotExist: raise ValidationError(self.error_messages['does_not_exist']) except (TypeError, ValueError): - msg = self.error_messages['invalid'] - raise ValidationError(msg) + msg = self.error_messages['incorrect_type'] + raise ValidationError(msg % type(value).__name__) return obj diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py index 7d65eae7..6d137f68 100644 --- a/rest_framework/tests/relations_hyperlink.py +++ b/rest_framework/tests/relations_hyperlink.py @@ -215,6 +215,13 @@ class HyperlinkedForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_incorrect_type(self): + data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': 2} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected url string, received int.']}) + 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) @@ -227,7 +234,7 @@ class HyperlinkedForeignKeyTests(TestCase): 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() diff --git a/rest_framework/tests/relations_pk.py b/rest_framework/tests/relations_pk.py index dd1e86b5..3391e60a 100644 --- a/rest_framework/tests/relations_pk.py +++ b/rest_framework/tests/relations_pk.py @@ -194,6 +194,13 @@ class PKForeignKeyTests(TestCase): ] self.assertEquals(serializer.data, expected) + def test_foreign_key_update_incorrect_type(self): + data = {'id': 1, 'name': u'source-1', 'target': 'foo'} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Incorrect type. Expected pk value, received str.']}) + def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': u'target-2', 'sources': [1, 3]} instance = ForeignKeyTarget.objects.get(pk=2) diff --git a/rest_framework/tests/relations_slug.py b/rest_framework/tests/relations_slug.py index 503b61e8..37ccc75e 100644 --- a/rest_framework/tests/relations_slug.py +++ b/rest_framework/tests/relations_slug.py @@ -1,9 +1,23 @@ from django.test import TestCase from rest_framework import serializers -from rest_framework.tests.models import NullableForeignKeySource, ForeignKeyTarget +from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget -class NullableSlugSourceSerializer(serializers.ModelSerializer): +class ForeignKeyTargetSerializer(serializers.ModelSerializer): + sources = serializers.ManySlugRelatedField(slug_field='name') + + class Meta: + model = ForeignKeyTarget + + +class ForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.SlugRelatedField(slug_field='name') + + class Meta: + model = ForeignKeySource + + +class NullableForeignKeySourceSerializer(serializers.ModelSerializer): target = serializers.SlugRelatedField(slug_field='name', null=True) class Meta: @@ -11,6 +25,132 @@ class NullableSlugSourceSerializer(serializers.ModelSerializer): # TODO: M2M Tests, FKTests (Non-nulable), One2One +class PKForeignKeyTests(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() + + def test_foreign_key_retrieve(self): + queryset = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'} + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_retrieve(self): + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, + {'id': 2, 'name': u'target-2', 'sources': []}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update(self): + data = {'id': 1, 'name': u'source-1', 'target': 'target-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 = [ + {'id': 1, 'name': u'source-1', 'target': 'target-2'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'} + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_update_incorrect_type(self): + data = {'id': 1, 'name': u'source-1', 'target': 123} + instance = ForeignKeySource.objects.get(pk=1) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Object with name=123 does not exist.']}) + + def test_reverse_foreign_key_update(self): + data = {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']} + 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 = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-1', 'source-2', 'source-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() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': u'target-2', 'sources': ['source-1', 'source-3']}, + ] + self.assertEquals(serializer.data, expected) + + def test_foreign_key_create(self): + data = {'id': 4, 'name': u'source-4', 'target': 'target-2'} + serializer = ForeignKeySourceSerializer(data=data) + serializer.is_valid() + 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 = ForeignKeySource.objects.all() + serializer = ForeignKeySourceSerializer(queryset) + expected = [ + {'id': 1, 'name': u'source-1', 'target': 'target-1'}, + {'id': 2, 'name': u'source-2', 'target': 'target-1'}, + {'id': 3, 'name': u'source-3', 'target': 'target-1'}, + {'id': 4, 'name': u'source-4', 'target': 'target-2'}, + ] + self.assertEquals(serializer.data, expected) + + def test_reverse_foreign_key_create(self): + data = {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-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 3 is added, and everything else is as expected + queryset = ForeignKeyTarget.objects.all() + serializer = ForeignKeyTargetSerializer(queryset) + expected = [ + {'id': 1, 'name': u'target-1', 'sources': ['source-2']}, + {'id': 2, 'name': u'target-2', 'sources': []}, + {'id': 3, 'name': u'target-3', 'sources': ['source-1', 'source-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) + serializer = ForeignKeySourceSerializer(instance, data=data) + self.assertFalse(serializer.is_valid()) + self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + class SlugNullableForeignKeyTests(TestCase): def setUp(self): @@ -24,7 +164,7 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_retrieve_with_null(self): queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -34,7 +174,7 @@ class SlugNullableForeignKeyTests(TestCase): def test_foreign_key_create_with_valid_null(self): data = {'id': 4, 'name': u'source-4', 'target': None} - serializer = NullableSlugSourceSerializer(data=data) + serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, data) @@ -42,7 +182,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -58,7 +198,7 @@ class SlugNullableForeignKeyTests(TestCase): """ data = {'id': 4, 'name': u'source-4', 'target': ''} expected_data = {'id': 4, 'name': u'source-4', 'target': None} - serializer = NullableSlugSourceSerializer(data=data) + serializer = NullableForeignKeySourceSerializer(data=data) self.assertTrue(serializer.is_valid()) obj = serializer.save() self.assertEquals(serializer.data, expected_data) @@ -66,7 +206,7 @@ class SlugNullableForeignKeyTests(TestCase): # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() - serializer = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': 'target-1'}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -78,14 +218,14 @@ class SlugNullableForeignKeyTests(TestCase): 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 = NullableSlugSourceSerializer(instance, data=data) + 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 = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, @@ -101,14 +241,14 @@ class SlugNullableForeignKeyTests(TestCase): 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 = NullableSlugSourceSerializer(instance, data=data) + 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 = NullableSlugSourceSerializer(queryset) + serializer = NullableForeignKeySourceSerializer(queryset) expected = [ {'id': 1, 'name': u'source-1', 'target': None}, {'id': 2, 'name': u'source-2', 'target': 'target-1'}, -- cgit v1.2.3 From a98049c5de9a4ac9e93eac9798e00df9c93caf81 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 19 Jan 2013 15:25:32 +0000 Subject: Drop unneeded test --- rest_framework/tests/decorators.py | 8 -------- 1 file changed, 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 5e6bce4e..4012188d 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -28,14 +28,6 @@ class DecoratorTestCase(TestCase): response.request = request return APIView.finalize_response(self, request, response, *args, **kwargs) - def test_wrap_view(self): - - @api_view(['GET']) - def view(request): - return Response({}) - - self.assertTrue(isinstance(view.cls_instance, APIView)) - def test_calling_method(self): @api_view(['GET']) -- cgit v1.2.3 From 37d49429ca34eed86ea142e5dceea4cd9536df2d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 19 Jan 2013 15:51:14 +0000 Subject: Raise assertion errors if @api_view decorator is applied incorrectly. Fixes #596. --- rest_framework/decorators.py | 9 +++++++++ rest_framework/tests/decorators.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 1b710a03..7a4103e1 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -1,4 +1,5 @@ from rest_framework.views import APIView +import types def api_view(http_method_names): @@ -23,6 +24,14 @@ def api_view(http_method_names): # pass # WrappedAPIView.__doc__ = func.doc <--- Not possible to do this + # api_view applied without (method_names) + assert not(isinstance(http_method_names, types.FunctionType)), \ + '@api_view missing list of allowed HTTP methods' + + # api_view applied with eg. string instead of list of strings + assert isinstance(http_method_names, (list, tuple)), \ + '@api_view expected a list of strings, recieved %s' % type(http_method_names).__name__ + allowed_methods = set(http_method_names) | set(('options',)) WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 4012188d..82f912e9 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -28,6 +28,28 @@ class DecoratorTestCase(TestCase): response.request = request return APIView.finalize_response(self, request, response, *args, **kwargs) + def test_api_view_incorrect(self): + """ + If @api_view is not applied correct, we should raise an assertion. + """ + + @api_view + def view(request): + return Response() + + request = self.factory.get('/') + self.assertRaises(AssertionError, view, request) + + def test_api_view_incorrect_arguments(self): + """ + If @api_view is missing arguments, we should raise an assertion. + """ + + with self.assertRaises(AssertionError): + @api_view('GET') + def view(request): + return Response() + def test_calling_method(self): @api_view(['GET']) -- cgit v1.2.3 From 2c05faa52ae65f96fdcc73efceb6c44511698261 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 19 Jan 2013 16:56:48 +0000 Subject: `format_suffix_patterns` now support `include`-style nested URL patterns. Fixes #593 --- rest_framework/urlpatterns.py | 44 ++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 143928c9..0aaad334 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,5 +1,34 @@ -from rest_framework.compat import url +from rest_framework.compat import url, include from rest_framework.settings import api_settings +from django.core.urlresolvers import RegexURLResolver + + +def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): + ret = [] + for urlpattern in urlpatterns: + if not isinstance(urlpattern, RegexURLResolver): + # Regular URL pattern + + # Form our complementing '.format' urlpattern + regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern + view = urlpattern._callback or urlpattern._callback_str + kwargs = urlpattern.default_args + name = urlpattern.name + # Add in both the existing and the new urlpattern + if not suffix_required: + ret.append(urlpattern) + ret.append(url(regex, view, kwargs, name)) + else: + # Set of included URL patterns + print(type(urlpattern)) + regex = urlpattern.regex.pattern + namespace = urlpattern.namespace + app_name = urlpattern.app_name + patterns = apply_suffix_patterns(urlpattern.url_patterns, + suffix_pattern, + suffix_required) + ret.append(url(regex, include(patterns, namespace, app_name))) + return ret def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): @@ -28,15 +57,4 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): else: suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg - ret = [] - for urlpattern in urlpatterns: - # Form our complementing '.format' urlpattern - regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern - view = urlpattern._callback or urlpattern._callback_str - kwargs = urlpattern.default_args - name = urlpattern.name - # Add in both the existing and the new urlpattern - if not suffix_required: - ret.append(urlpattern) - ret.append(url(regex, view, kwargs, name)) - return ret + return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) -- cgit v1.2.3 From 69083c3668b363bd9cb85674255d260808bbeeff Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 19 Jan 2013 18:36:25 +0000 Subject: Drop print statement --- rest_framework/urlpatterns.py | 1 - 1 file changed, 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 0aaad334..162f2314 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -20,7 +20,6 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): ret.append(url(regex, view, kwargs, name)) else: # Set of included URL patterns - print(type(urlpattern)) regex = urlpattern.regex.pattern namespace = urlpattern.namespace app_name = urlpattern.app_name -- cgit v1.2.3 From 771821af7d8eb6751d6ea37eabae7108cebc0df0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 19 Jan 2013 18:39:39 +0000 Subject: Include kwargs in included URLs --- rest_framework/urlpatterns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 162f2314..0f210e66 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -23,10 +23,11 @@ def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): regex = urlpattern.regex.pattern namespace = urlpattern.namespace app_name = urlpattern.app_name + kwargs = urlpattern.default_kwargs patterns = apply_suffix_patterns(urlpattern.url_patterns, suffix_pattern, suffix_required) - ret.append(url(regex, include(patterns, namespace, app_name))) + ret.append(url(regex, include(patterns, namespace, app_name), kwargs)) return ret -- cgit v1.2.3 From 71bd2faa792569c9f4c83a06904b927616bfdbf1 Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Sun, 20 Jan 2013 12:59:27 -0800 Subject: Added test case for format_suffix_patterns to validate changes introduced with issue #593. Signed-off-by: Kevin Stone --- rest_framework/tests/urlpatterns.py | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 rest_framework/tests/urlpatterns.py (limited to 'rest_framework') diff --git a/rest_framework/tests/urlpatterns.py b/rest_framework/tests/urlpatterns.py new file mode 100644 index 00000000..e96e7cf3 --- /dev/null +++ b/rest_framework/tests/urlpatterns.py @@ -0,0 +1,75 @@ +from collections import namedtuple + +from django.core import urlresolvers + +from django.test import TestCase +from django.test.client import RequestFactory + +from rest_framework.compat import patterns, url, include +from rest_framework.urlpatterns import format_suffix_patterns + + +# A container class for test paths for the test case +URLTestPath = namedtuple('URLTestPath', ['path', 'args', 'kwargs']) + + +def test_view(request, *args, **kwargs): + pass + + +class FormatSuffixTests(TestCase): + def _test_urlpatterns(self, urlpatterns, test_paths): + factory = RequestFactory() + try: + urlpatterns = format_suffix_patterns(urlpatterns) + except: + self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") + resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) + for test_path in test_paths: + request = factory.get(test_path.path) + try: + callback, callback_args, callback_kwargs = resolver.resolve(request.path_info) + except: + self.fail("Failed to resolve URL: %s" % request.path_info) + self.assertEquals(callback_args, test_path.args) + self.assertEquals(callback_kwargs, test_path.kwargs) + + def test_format_suffix(self): + urlpatterns = patterns( + '', + url(r'^test$', test_view), + ) + test_paths = [ + URLTestPath('/test', (), {}), + URLTestPath('/test.api', (), {'format': 'api'}), + URLTestPath('/test.asdf', (), {'format': 'asdf'}), + ] + self._test_urlpatterns(urlpatterns, test_paths) + + def test_default_args(self): + urlpatterns = patterns( + '', + url(r'^test$', test_view, {'foo': 'bar'}), + ) + test_paths = [ + URLTestPath('/test', (), {'foo': 'bar', }), + URLTestPath('/test.api', (), {'foo': 'bar', 'format': 'api'}), + URLTestPath('/test.asdf', (), {'foo': 'bar', 'format': 'asdf'}), + ] + self._test_urlpatterns(urlpatterns, test_paths) + + def test_included_urls(self): + nested_patterns = patterns( + '', + url(r'^path$', test_view) + ) + urlpatterns = patterns( + '', + url(r'^test/', include(nested_patterns), {'foo': 'bar'}), + ) + test_paths = [ + URLTestPath('/test/path', (), {'foo': 'bar', }), + URLTestPath('/test/path.api', (), {'foo': 'bar', 'format': 'api'}), + URLTestPath('/test/path.asdf', (), {'foo': 'bar', 'format': 'asdf'}), + ] + self._test_urlpatterns(urlpatterns, test_paths) -- cgit v1.2.3 From e7916ae0b1c4af35c55dc21e0d882f3f8ff3121e Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Mon, 21 Jan 2013 09:37:50 -0800 Subject: Tweaked some method names to be more clear and added a docstring to the test case class. Signed-off-by: Kevin Stone --- rest_framework/tests/urlpatterns.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/urlpatterns.py b/rest_framework/tests/urlpatterns.py index e96e7cf3..43e8ef69 100644 --- a/rest_framework/tests/urlpatterns.py +++ b/rest_framework/tests/urlpatterns.py @@ -13,12 +13,15 @@ from rest_framework.urlpatterns import format_suffix_patterns URLTestPath = namedtuple('URLTestPath', ['path', 'args', 'kwargs']) -def test_view(request, *args, **kwargs): +def dummy_view(request, *args, **kwargs): pass class FormatSuffixTests(TestCase): - def _test_urlpatterns(self, urlpatterns, test_paths): + """ + Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters. + """ + def _resolve_urlpatterns(self, urlpatterns, test_paths): factory = RequestFactory() try: urlpatterns = format_suffix_patterns(urlpatterns) @@ -37,31 +40,31 @@ class FormatSuffixTests(TestCase): def test_format_suffix(self): urlpatterns = patterns( '', - url(r'^test$', test_view), + url(r'^test$', dummy_view), ) test_paths = [ URLTestPath('/test', (), {}), URLTestPath('/test.api', (), {'format': 'api'}), URLTestPath('/test.asdf', (), {'format': 'asdf'}), ] - self._test_urlpatterns(urlpatterns, test_paths) + self._resolve_urlpatterns(urlpatterns, test_paths) def test_default_args(self): urlpatterns = patterns( '', - url(r'^test$', test_view, {'foo': 'bar'}), + url(r'^test$', dummy_view, {'foo': 'bar'}), ) test_paths = [ URLTestPath('/test', (), {'foo': 'bar', }), URLTestPath('/test.api', (), {'foo': 'bar', 'format': 'api'}), URLTestPath('/test.asdf', (), {'foo': 'bar', 'format': 'asdf'}), ] - self._test_urlpatterns(urlpatterns, test_paths) + self._resolve_urlpatterns(urlpatterns, test_paths) def test_included_urls(self): nested_patterns = patterns( '', - url(r'^path$', test_view) + url(r'^path$', dummy_view) ) urlpatterns = patterns( '', @@ -72,4 +75,4 @@ class FormatSuffixTests(TestCase): URLTestPath('/test/path.api', (), {'foo': 'bar', 'format': 'api'}), URLTestPath('/test/path.asdf', (), {'foo': 'bar', 'format': 'asdf'}), ] - self._test_urlpatterns(urlpatterns, test_paths) + self._resolve_urlpatterns(urlpatterns, test_paths) -- cgit v1.2.3 From 98bffa68e655e530c16e4622658541940b3891f0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Jan 2013 17:42:33 +0000 Subject: Don't do an inverted if test. --- rest_framework/urlpatterns.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 0f210e66..47789026 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -6,28 +6,29 @@ from django.core.urlresolvers import RegexURLResolver def apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required): ret = [] for urlpattern in urlpatterns: - if not isinstance(urlpattern, RegexURLResolver): - # Regular URL pattern - - # Form our complementing '.format' urlpattern - regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern - view = urlpattern._callback or urlpattern._callback_str - kwargs = urlpattern.default_args - name = urlpattern.name - # Add in both the existing and the new urlpattern - if not suffix_required: - ret.append(urlpattern) - ret.append(url(regex, view, kwargs, name)) - else: + if isinstance(urlpattern, RegexURLResolver): # Set of included URL patterns regex = urlpattern.regex.pattern namespace = urlpattern.namespace app_name = urlpattern.app_name kwargs = urlpattern.default_kwargs + # Add in the included patterns, after applying the suffixes patterns = apply_suffix_patterns(urlpattern.url_patterns, suffix_pattern, suffix_required) ret.append(url(regex, include(patterns, namespace, app_name), kwargs)) + + else: + # Regular URL pattern + regex = urlpattern.regex.pattern.rstrip('$') + suffix_pattern + view = urlpattern._callback or urlpattern._callback_str + kwargs = urlpattern.default_args + name = urlpattern.name + # Add in both the existing and the new urlpattern + if not suffix_required: + ret.append(urlpattern) + ret.append(url(regex, view, kwargs, name)) + return ret -- cgit v1.2.3