diff --git a/drf_writable_nested/mixins.py b/drf_writable_nested/mixins.py index 27577ae..d8eb574 100644 --- a/drf_writable_nested/mixins.py +++ b/drf_writable_nested/mixins.py @@ -115,6 +115,9 @@ def update_or_create_reverse_relations(self, instance, reverse_relations): if related_data is None: # Skip processing for empty data continue + pk_name = field.Meta.model._meta.pk.attname + if pk_name not in related_data: + related_data[pk_name] = instance.pk related_data = [related_data] instances = self.prefetch_related_instances(field, related_data) diff --git a/tests/test_writable_nested_model_serializer.py b/tests/test_writable_nested_model_serializer.py index 6cf0c8b..f4bfbd0 100644 --- a/tests/test_writable_nested_model_serializer.py +++ b/tests/test_writable_nested_model_serializer.py @@ -219,6 +219,112 @@ def test_update(self): # Sites shouldn't be deleted either as it is M2M self.assertEqual(models.Site.objects.count(), 3) + def test_update_reverse_one_to_one_without_pk(self): + serializer = serializers.UserSerializer(data=self.get_initial_data()) + serializer.is_valid(raise_exception=True) + user = serializer.save() + + # Check instances count + self.assertEqual(models.User.objects.count(), 1) + self.assertEqual(models.Profile.objects.count(), 1) + self.assertEqual(models.Site.objects.count(), 2) + self.assertEqual(models.Avatar.objects.count(), 2) + self.assertEqual(models.Message.objects.count(), 3) + + # Update + user_pk = user.pk + profile_pk = user.profile.pk + + message_to_update_str_pk = str(user.profile.messages.first().pk) + message_to_update_pk = user.profile.messages.last().pk + serializer = serializers.UserSerializer( + instance=user, + data={ + 'pk': user_pk, + 'username': 'new', + 'profile': { + # omit pk + 'access_key': None, + 'sites': [ + { + 'url': 'http://new-site.com', + }, + ], + 'avatars': [ + { + 'pk': user.profile.avatars.earliest('pk').pk, + 'image': 'old-image-1.png', + }, + { + 'image': 'new-image-1.png', + }, + { + 'image': 'new-image-2.png', + }, + ], + 'messages': [ + { + 'pk': message_to_update_str_pk, + 'message': 'Old message 1' + }, + { + 'pk': message_to_update_pk, + 'message': 'Old message 2' + }, + { + 'message': 'New message 1' + } + ], + }, + }, + ) + + serializer.is_valid(raise_exception=True) + user = serializer.save() + user.refresh_from_db() + self.assertIsNotNone(user) + self.assertEqual(user.pk, user_pk) + self.assertEqual(user.username, 'new') + + profile = user.profile + self.assertIsNotNone(profile) + self.assertIsNone(profile.access_key) + self.assertEqual(profile.pk, profile_pk) + self.assertEqual(profile.sites.count(), 1) + self.assertSetEqual( + set(profile.sites.values_list('url', flat=True)), + {'http://new-site.com'} + ) + self.assertEqual(profile.avatars.count(), 3) + self.assertSetEqual( + set(profile.avatars.values_list('image', flat=True)), + {'old-image-1.png', 'new-image-1.png', 'new-image-2.png'} + ) + self.assertSetEqual( + set(profile.messages.values_list('message', flat=True)), + {'Old message 1', 'Old message 2', 'New message 1'} + ) + # Check that message which supposed to be updated still in profile + # messages (new message wasn't created instead of update) + self.assertIn( + message_to_update_pk, + profile.messages.values_list('id', flat=True) + ) + self.assertIn( + uuid.UUID(message_to_update_str_pk), + profile.messages.values_list('id', flat=True) + ) + + # Check instances count + self.assertEqual(models.User.objects.count(), 1) + self.assertEqual(models.Profile.objects.count(), 1) + self.assertEqual(models.Avatar.objects.count(), 3) + self.assertEqual(models.Message.objects.count(), 3) + # Access key shouldn't be removed because it is FK + self.assertEqual(models.AccessKey.objects.count(), 1) + # Sites shouldn't be deleted either as it is M2M + self.assertEqual(models.Site.objects.count(), 3) + def test_update_raise_protected_error(self): serializer = serializers.UserSerializer(data=self.get_initial_data()) serializer.is_valid(raise_exception=True)