From 34d2f7c529c33bc05eeb48e14fb42f4683c6348a Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Thu, 13 Jul 2017 15:40:42 -0400 Subject: [PATCH] Fix issue when serializing certain foreign keys. If a foreign key has a default value that is not an instance of the related model (i.e. its a value for the primary key for that model), the foreign key field has a uniqueness constraint, and the field was not provided as data for the serializer, then we need to set that default value to the attname of the field when creating the model instance. Otherwise, a ValueError will be raised when creating the model instance, saying 'Cannot assign "": "" must be a "" instance.' This change ensures that the kwargs passed to model instance creation are modified when appropriate to account for this. Signed-off-by: Tommy Beadle --- rest_framework/serializers.py | 12 ++++++++--- tests/models.py | 13 ++++++++++++ tests/test_relations_pk.py | 40 +++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a4b51ae9d..c1dc67155 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -909,8 +909,14 @@ class ModelSerializer(Serializer): info = model_meta.get_field_info(ModelClass) many_to_many = {} for field_name, relation_info in info.relations.items(): - if relation_info.to_many and (field_name in validated_data): - many_to_many[field_name] = validated_data.pop(field_name) + if field_name in validated_data: + if relation_info.to_many: + many_to_many[field_name] = validated_data.pop(field_name) + elif not isinstance( + validated_data[field_name], + relation_info.related_model): + validated_data[relation_info.model_field.attname] = \ + validated_data.pop(field_name) try: instance = ModelClass.objects.create(**validated_data) @@ -1368,7 +1374,7 @@ class ModelSerializer(Serializer): elif getattr(unique_constraint_field, 'auto_now', None): default = timezone.now elif unique_constraint_field.has_default(): - default = unique_constraint_field.default + default = unique_constraint_field.get_default() else: default = empty diff --git a/tests/models.py b/tests/models.py index 6c9dde8fa..7ea44c650 100644 --- a/tests/models.py +++ b/tests/models.py @@ -61,6 +61,19 @@ class ForeignKeySource(RESTFrameworkModel): on_delete=models.CASCADE) +class ForeignKeyInUniquenessConstraintSource(RESTFrameworkModel): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, + related_name='sources_with_uniqueness', + help_text='Target', + verbose_name='Target', + default=1, + on_delete=models.CASCADE) + + class Meta: + unique_together = ('name', 'target') + + # Nullable ForeignKey class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 2eebe1b5c..cdc7f2aa2 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -5,8 +5,9 @@ from django.utils import six from rest_framework import serializers from tests.models import ( - ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget, - NullableForeignKeySource, NullableOneToOneSource, NullableUUIDForeignKeySource, + ForeignKeyInUniquenessConstraintSource, ForeignKeySource, ForeignKeyTarget, + ManyToManySource, ManyToManyTarget, NullableForeignKeySource, + NullableOneToOneSource, NullableUUIDForeignKeySource, OneToOnePKSource, OneToOneTarget, UUIDForeignKeyTarget ) @@ -37,6 +38,12 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'target') +class ForeignKeyUniquenessSourceSerializer(serializers.ModelSerializer): + class Meta: + model = ForeignKeyInUniquenessConstraintSource + fields = ('id', 'name', 'target') + + # Nullable ForeignKey class NullableForeignKeySourceSerializer(serializers.ModelSerializer): class Meta: @@ -360,6 +367,35 @@ class PKForeignKeyTests(TestCase): assert 'target' not in serializer.validated_data +class PKForeignKeyInUniquenessConstraintTests(TestCase): + def test_use_default_value_for_fk(self): + default_val = ForeignKeyInUniquenessConstraintSource._meta.get_field( + 'target' + ).default + target = ForeignKeyTarget.objects.create(pk=default_val) + serializer = ForeignKeyUniquenessSourceSerializer( + data={'name': 'source-1'} + ) + assert serializer.is_valid() + serializer.save() + source = ForeignKeyInUniquenessConstraintSource.objects.get( + name='source-1', + ) + assert source.target == target + + def test_provide_value_for_fk(self): + target = ForeignKeyTarget.objects.create(pk=10) + serializer = ForeignKeyUniquenessSourceSerializer( + data={'name': 'source-2', 'target': target.pk} + ) + assert serializer.is_valid() + serializer.save() + source = ForeignKeyInUniquenessConstraintSource.objects.get( + name='source-2', + ) + assert source.target == target + + class PKNullableForeignKeyTests(TestCase): def setUp(self): target = ForeignKeyTarget(name='target-1')