diff --git a/tests/migrations/0003_i49product_i49veterinary_alter_accesskey_id_and_more.py b/tests/migrations/0003_i49product_i49veterinary_alter_accesskey_id_and_more.py new file mode 100644 index 0000000..4426be2 --- /dev/null +++ b/tests/migrations/0003_i49product_i49veterinary_alter_accesskey_id_and_more.py @@ -0,0 +1,239 @@ +# Generated by Django 4.2.6 on 2023-10-31 17:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("tests", "0002_alter_profile_sites_setnullforeignkey_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="I49Product", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("number", models.CharField(max_length=50)), + ("cost", models.FloatField()), + ], + ), + migrations.CreateModel( + name="I49Veterinary", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=50)), + ], + ), + migrations.AlterField( + model_name="accesskey", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="anotheravatar", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="anotherprofile", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="avatar", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="custompk", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="document", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="foreignkeychild", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="foreignkeyparent", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="i86genre", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="i86name", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="manytomanychild", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="manytomanyparent", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="onetoonechild", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="onetooneparent", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="page", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="profile", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="site", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="tag", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="taggeditem", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="team", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="ufmchild", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="ufmparent", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="user", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.CreateModel( + name="I49ProductDetail", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("cost", models.FloatField()), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="product_details", + to="tests.i49product", + ), + ), + ( + "veterinary", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="tests.i49veterinary", + ), + ), + ], + options={ + "unique_together": {("product", "veterinary")}, + }, + ), + ] diff --git a/tests/models.py b/tests/models.py index df28c1e..0ee41fc 100644 --- a/tests/models.py +++ b/tests/models.py @@ -168,3 +168,20 @@ class I86Name(models.Model): class I86Genre(models.Model): pass + +class I49Veterinary(models.Model): + name = models.CharField(max_length=50) + +class I49Product(models.Model): + number = models.CharField(max_length=50) + cost = models.FloatField() + + +class I49ProductDetail(models.Model): + product = models.ForeignKey("I49Product", on_delete=models.CASCADE, related_name='product_details') + veterinary = models.ForeignKey("I49Veterinary", on_delete=models.PROTECT) + cost = models.FloatField() + + class Meta: + unique_together = ('product', 'veterinary') + \ No newline at end of file diff --git a/tests/serializers.py b/tests/serializers.py index 811e035..4bbbbdf 100644 --- a/tests/serializers.py +++ b/tests/serializers.py @@ -2,6 +2,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.validators import UniqueValidator +from rest_framework.relations import PrimaryKeyRelatedField from drf_writable_nested.serializers import WritableNestedModelSerializer from drf_writable_nested.mixins import UniqueFieldsMixin @@ -374,3 +375,21 @@ class I86GenreSerializer(WritableNestedModelSerializer): class Meta: model = models.I86Genre fields = ('id', 'names',) + + + +class I49ProductDetailSerializer(serializers.ModelSerializer): + id = serializers.IntegerField(required=False) + veterinary = PrimaryKeyRelatedField(queryset=models.I49Veterinary.objects) + + class Meta: + model = models.I49ProductDetail + fields = ('id', 'veterinary', 'cost') + + +class I49ProductSerializerWithPK(WritableNestedModelSerializer): + product_details = I49ProductDetailSerializer(many=True) + + class Meta: + model = models.I49Product + fields = ('id', 'number', 'cost', 'product_details') diff --git a/tests/test_unique_fields_mixin.py b/tests/test_unique_fields_mixin.py index 4a036aa..4f3c6f9 100644 --- a/tests/test_unique_fields_mixin.py +++ b/tests/test_unique_fields_mixin.py @@ -88,16 +88,46 @@ def test_create_update_failed(self): with self.assertRaises(ValidationError) as ctx: serializer.save() self.assertEqual( - ctx.exception.detail, - {'child': {'field': [unique_message_error_detail]}} + ctx.exception.detail, {"child": {"field": [unique_message_error_detail]}} ) def test_unique_field_not_required_for_partial_updates(self): - child = models.UFMChild.objects.create(field='value') + child = models.UFMChild.objects.create(field="value") serializer = serializers.UFMChildSerializer( - instance=child, - data={}, - partial=True + instance=child, data={}, partial=True ) self.assertTrue(serializer.is_valid()) serializer.save() + + +class I49Test(TestCase): + def test_issue_49(self): + veterinary = models.I49Veterinary.objects.create(name="Veterinary 1") + serializer = serializers.I49ProductSerializerWithPK( + data={ + "number": "Product XX", + "cost": 20000, + "product_details": [{"veterinary": veterinary.id, "cost": 10000}], + } + ) + + self.assertTrue(serializer.is_valid()) + instance = serializer.save() + + assert len(models.I49Product.objects.all()) == 1 + assert len(models.I49ProductDetail.objects.all()) == 1 + + serializer = serializers.I49ProductSerializerWithPK( + instance=instance, + data={ + "number": "Product XX", + "cost": 20000, + "product_details": [{"veterinary": veterinary.id, "cost": 10000}], + }, + ) + + self.assertTrue(serializer.is_valid()) + serializer.save() + + assert len(models.I49Product.objects.all()) == 1 + assert len(models.I49ProductDetail.objects.all()) == 1