From 78f2cf86598c55a0606a49f703ffe4b951c27197 Mon Sep 17 00:00:00 2001 From: Andrea Zanotto Date: Wed, 2 Apr 2025 10:49:27 +0200 Subject: [PATCH 1/3] TestUniqueConstraintValidation failing on foreign key field. --- tests/test_validators.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 29b097ef39..b0d71b91ff 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -517,11 +517,15 @@ def filter(self, **kwargs): assert queryset.called_with == {'race_name': 'bar', 'position': 1} +class FancyConditionModel(models.Model): + id = models.IntegerField(primary_key=True) + + class UniqueConstraintModel(models.Model): race_name = models.CharField(max_length=100) position = models.IntegerField() global_id = models.IntegerField() - fancy_conditions = models.IntegerField() + fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE) class Meta: constraints = [ @@ -578,23 +582,24 @@ class Meta: class TestUniqueConstraintValidation(TestCase): def setUp(self): + fancy_model_condition = FancyConditionModel.objects.create(id=1) self.instance = UniqueConstraintModel.objects.create( race_name='example', position=1, global_id=1, - fancy_conditions=1 + fancy_conditions=fancy_model_condition ) UniqueConstraintModel.objects.create( race_name='example', position=2, global_id=2, - fancy_conditions=1 + fancy_conditions=fancy_model_condition ) UniqueConstraintModel.objects.create( race_name='other', position=1, global_id=3, - fancy_conditions=1 + fancy_conditions=fancy_model_condition ) def test_repr(self): @@ -618,24 +623,27 @@ def test_unique_together_condition(self): Fields used in UniqueConstraint's condition must be included into queryset existence check """ + fancy_model_condition_9 = FancyConditionModel.objects.create(id=9) + fancy_model_condition_10 = FancyConditionModel.objects.create(id=10) + fancy_model_condition_11 = FancyConditionModel.objects.create(id=11) UniqueConstraintModel.objects.create( race_name='condition', position=1, global_id=10, - fancy_conditions=10, + fancy_conditions=fancy_model_condition_10, ) serializer = UniqueConstraintSerializer(data={ 'race_name': 'condition', 'position': 1, 'global_id': 11, - 'fancy_conditions': 9, + 'fancy_conditions': fancy_model_condition_9, }) assert serializer.is_valid() serializer = UniqueConstraintSerializer(data={ 'race_name': 'condition', 'position': 1, 'global_id': 11, - 'fancy_conditions': 11, + 'fancy_conditions': fancy_model_condition_11, }) assert not serializer.is_valid() From 68af9408b7ca4dc64444c940ea99a1749c6a8a0a Mon Sep 17 00:00:00 2001 From: Andrea Zanotto Date: Tue, 8 Apr 2025 09:45:45 +0200 Subject: [PATCH 2/3] Revert "TestUniqueConstraintValidation failing on foreign key field." This reverts commit 78f2cf86598c55a0606a49f703ffe4b951c27197. --- tests/test_validators.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index b0d71b91ff..29b097ef39 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -517,15 +517,11 @@ def filter(self, **kwargs): assert queryset.called_with == {'race_name': 'bar', 'position': 1} -class FancyConditionModel(models.Model): - id = models.IntegerField(primary_key=True) - - class UniqueConstraintModel(models.Model): race_name = models.CharField(max_length=100) position = models.IntegerField() global_id = models.IntegerField() - fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE) + fancy_conditions = models.IntegerField() class Meta: constraints = [ @@ -582,24 +578,23 @@ class Meta: class TestUniqueConstraintValidation(TestCase): def setUp(self): - fancy_model_condition = FancyConditionModel.objects.create(id=1) self.instance = UniqueConstraintModel.objects.create( race_name='example', position=1, global_id=1, - fancy_conditions=fancy_model_condition + fancy_conditions=1 ) UniqueConstraintModel.objects.create( race_name='example', position=2, global_id=2, - fancy_conditions=fancy_model_condition + fancy_conditions=1 ) UniqueConstraintModel.objects.create( race_name='other', position=1, global_id=3, - fancy_conditions=fancy_model_condition + fancy_conditions=1 ) def test_repr(self): @@ -623,27 +618,24 @@ def test_unique_together_condition(self): Fields used in UniqueConstraint's condition must be included into queryset existence check """ - fancy_model_condition_9 = FancyConditionModel.objects.create(id=9) - fancy_model_condition_10 = FancyConditionModel.objects.create(id=10) - fancy_model_condition_11 = FancyConditionModel.objects.create(id=11) UniqueConstraintModel.objects.create( race_name='condition', position=1, global_id=10, - fancy_conditions=fancy_model_condition_10, + fancy_conditions=10, ) serializer = UniqueConstraintSerializer(data={ 'race_name': 'condition', 'position': 1, 'global_id': 11, - 'fancy_conditions': fancy_model_condition_9, + 'fancy_conditions': 9, }) assert serializer.is_valid() serializer = UniqueConstraintSerializer(data={ 'race_name': 'condition', 'position': 1, 'global_id': 11, - 'fancy_conditions': fancy_model_condition_11, + 'fancy_conditions': 11, }) assert not serializer.is_valid() From 4920fd5338bdc8f2c7904d8d78a819a6282bf3c3 Mon Sep 17 00:00:00 2001 From: Andrea Zanotto Date: Tue, 8 Apr 2025 09:49:19 +0200 Subject: [PATCH 3/3] Add TestUniqueConstraintForeignKeyValidation test failing on foreign key field. --- tests/test_validators.py | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/tests/test_validators.py b/tests/test_validators.py index 29b097ef39..e40ed5dc82 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -552,6 +552,45 @@ class Meta: ] +class FancyConditionModel(models.Model): + id = models.IntegerField(primary_key=True) + + +class UniqueConstraintForeignKeyModel(models.Model): + race_name = models.CharField(max_length=100) + position = models.IntegerField() + global_id = models.IntegerField() + fancy_conditions = models.ForeignKey(FancyConditionModel, on_delete=models.CASCADE) + + class Meta: + constraints = [ + models.UniqueConstraint( + name="unique_constraint_foreign_key_model_global_id_uniq", + fields=('global_id',), + ), + models.UniqueConstraint( + name="unique_constraint_foreign_key_model_fancy_1_uniq", + fields=('fancy_conditions',), + condition=models.Q(global_id__lte=1) + ), + models.UniqueConstraint( + name="unique_constraint_foreign_key_model_fancy_3_uniq", + fields=('fancy_conditions',), + condition=models.Q(global_id__gte=3) + ), + models.UniqueConstraint( + name="unique_constraint_foreign_key_model_together_uniq", + fields=('race_name', 'position'), + condition=models.Q(race_name='example'), + ), + models.UniqueConstraint( + name='unique_constraint_foreign_key_model_together_uniq2', + fields=('race_name', 'position'), + condition=models.Q(fancy_conditions__gte=10), + ), + ] + + class UniqueConstraintNullableModel(models.Model): title = models.CharField(max_length=100) age = models.IntegerField(null=True) @@ -570,6 +609,12 @@ class Meta: fields = '__all__' +class UniqueConstraintForeignKeySerializer(serializers.ModelSerializer): + class Meta: + model = UniqueConstraintForeignKeyModel + fields = '__all__' + + class UniqueConstraintNullableSerializer(serializers.ModelSerializer): class Meta: model = UniqueConstraintNullableModel @@ -684,6 +729,118 @@ def test_nullable_unique_constraint_fields_are_not_required(self): self.assertIsInstance(result, UniqueConstraintNullableModel) +class TestUniqueConstraintForeignKeyValidation(TestCase): + def setUp(self): + fancy_model_condition = FancyConditionModel.objects.create(id=1) + self.instance = UniqueConstraintForeignKeyModel.objects.create( + race_name='example', + position=1, + global_id=1, + fancy_conditions=fancy_model_condition + ) + UniqueConstraintForeignKeyModel.objects.create( + race_name='example', + position=2, + global_id=2, + fancy_conditions=fancy_model_condition + ) + UniqueConstraintForeignKeyModel.objects.create( + race_name='other', + position=1, + global_id=3, + fancy_conditions=fancy_model_condition + ) + + def test_repr(self): + serializer = UniqueConstraintForeignKeySerializer() + # the order of validators isn't deterministic so delete + # fancy_conditions field that has two of them + del serializer.fields['fancy_conditions'] + expected = dedent(r""" + UniqueConstraintForeignKeySerializer\(\): + id = IntegerField\(label='ID', read_only=True\) + race_name = CharField\(max_length=100, required=True\) + position = IntegerField\(.*required=True\) + global_id = IntegerField\(.*validators=\[\]\) + class Meta: + validators = \[\)>\] + """) + assert re.search(expected, repr(serializer)) is not None + + def test_unique_together_condition(self): + """ + Fields used in UniqueConstraint's condition must be included + into queryset existence check + """ + fancy_model_condition_9 = FancyConditionModel.objects.create(id=9) + fancy_model_condition_10 = FancyConditionModel.objects.create(id=10) + fancy_model_condition_11 = FancyConditionModel.objects.create(id=11) + UniqueConstraintForeignKeyModel.objects.create( + race_name='condition', + position=1, + global_id=10, + fancy_conditions=fancy_model_condition_10, + ) + serializer = UniqueConstraintForeignKeySerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + 'fancy_conditions': fancy_model_condition_9, + }) + assert serializer.is_valid() + serializer = UniqueConstraintForeignKeySerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + 'fancy_conditions': fancy_model_condition_11, + }) + assert not serializer.is_valid() + + def test_unique_together_condition_fields_required(self): + """ + Fields used in UniqueConstraint's condition must be present in serializer + """ + serializer = UniqueConstraintForeignKeySerializer(data={ + 'race_name': 'condition', + 'position': 1, + 'global_id': 11, + }) + assert not serializer.is_valid() + assert serializer.errors == {'fancy_conditions': ['This field is required.']} + + class NoFieldsSerializer(serializers.ModelSerializer): + class Meta: + model = UniqueConstraintForeignKeyModel + fields = ('race_name', 'position', 'global_id') + + serializer = NoFieldsSerializer() + assert len(serializer.validators) == 1 + + def test_single_field_uniq_validators(self): + """ + UniqueConstraint with single field must be transformed into + field's UniqueValidator + """ + # Django 5 includes Max and Min values validators for IntegerField + extra_validators_qty = 2 if django_version[0] >= 5 else 0 + serializer = UniqueConstraintForeignKeySerializer() + assert len(serializer.validators) == 2 + validators = serializer.fields['global_id'].validators + assert len(validators) == 1 + extra_validators_qty + assert validators[0].queryset == UniqueConstraintForeignKeyModel.objects + + validators = serializer.fields['fancy_conditions'].validators + assert len(validators) == 2 + extra_validators_qty + ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")} + assert ids_in_qs == {frozenset([1]), frozenset([3])} + + def test_nullable_unique_constraint_fields_are_not_required(self): + serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'}) + self.assertTrue(serializer.is_valid(), serializer.errors) + result = serializer.save() + self.assertIsInstance(result, UniqueConstraintNullableModel) + + # Tests for `UniqueForDateValidator` # ----------------------------------