@@ -552,6 +552,45 @@ class Meta:
552
552
]
553
553
554
554
555
+ class FancyConditionModel (models .Model ):
556
+ id = models .IntegerField (primary_key = True )
557
+
558
+
559
+ class UniqueConstraintForeignKeyModel (models .Model ):
560
+ race_name = models .CharField (max_length = 100 )
561
+ position = models .IntegerField ()
562
+ global_id = models .IntegerField ()
563
+ fancy_conditions = models .ForeignKey (FancyConditionModel , on_delete = models .CASCADE )
564
+
565
+ class Meta :
566
+ constraints = [
567
+ models .UniqueConstraint (
568
+ name = "unique_constraint_foreign_key_model_global_id_uniq" ,
569
+ fields = ('global_id' ,),
570
+ ),
571
+ models .UniqueConstraint (
572
+ name = "unique_constraint_foreign_key_model_fancy_1_uniq" ,
573
+ fields = ('fancy_conditions' ,),
574
+ condition = models .Q (global_id__lte = 1 )
575
+ ),
576
+ models .UniqueConstraint (
577
+ name = "unique_constraint_foreign_key_model_fancy_3_uniq" ,
578
+ fields = ('fancy_conditions' ,),
579
+ condition = models .Q (global_id__gte = 3 )
580
+ ),
581
+ models .UniqueConstraint (
582
+ name = "unique_constraint_foreign_key_model_together_uniq" ,
583
+ fields = ('race_name' , 'position' ),
584
+ condition = models .Q (race_name = 'example' ),
585
+ ),
586
+ models .UniqueConstraint (
587
+ name = 'unique_constraint_foreign_key_model_together_uniq2' ,
588
+ fields = ('race_name' , 'position' ),
589
+ condition = models .Q (fancy_conditions__gte = 10 ),
590
+ ),
591
+ ]
592
+
593
+
555
594
class UniqueConstraintNullableModel (models .Model ):
556
595
title = models .CharField (max_length = 100 )
557
596
age = models .IntegerField (null = True )
@@ -570,6 +609,12 @@ class Meta:
570
609
fields = '__all__'
571
610
572
611
612
+ class UniqueConstraintForeignKeySerializer (serializers .ModelSerializer ):
613
+ class Meta :
614
+ model = UniqueConstraintForeignKeyModel
615
+ fields = '__all__'
616
+
617
+
573
618
class UniqueConstraintNullableSerializer (serializers .ModelSerializer ):
574
619
class Meta :
575
620
model = UniqueConstraintNullableModel
@@ -684,6 +729,118 @@ def test_nullable_unique_constraint_fields_are_not_required(self):
684
729
self .assertIsInstance (result , UniqueConstraintNullableModel )
685
730
686
731
732
+ class TestUniqueConstraintForeignKeyValidation (TestCase ):
733
+ def setUp (self ):
734
+ fancy_model_condition = FancyConditionModel .objects .create (id = 1 )
735
+ self .instance = UniqueConstraintForeignKeyModel .objects .create (
736
+ race_name = 'example' ,
737
+ position = 1 ,
738
+ global_id = 1 ,
739
+ fancy_conditions = fancy_model_condition
740
+ )
741
+ UniqueConstraintForeignKeyModel .objects .create (
742
+ race_name = 'example' ,
743
+ position = 2 ,
744
+ global_id = 2 ,
745
+ fancy_conditions = fancy_model_condition
746
+ )
747
+ UniqueConstraintForeignKeyModel .objects .create (
748
+ race_name = 'other' ,
749
+ position = 1 ,
750
+ global_id = 3 ,
751
+ fancy_conditions = fancy_model_condition
752
+ )
753
+
754
+ def test_repr (self ):
755
+ serializer = UniqueConstraintForeignKeySerializer ()
756
+ # the order of validators isn't deterministic so delete
757
+ # fancy_conditions field that has two of them
758
+ del serializer .fields ['fancy_conditions' ]
759
+ expected = dedent (r"""
760
+ UniqueConstraintForeignKeySerializer\(\):
761
+ id = IntegerField\(label='ID', read_only=True\)
762
+ race_name = CharField\(max_length=100, required=True\)
763
+ position = IntegerField\(.*required=True\)
764
+ global_id = IntegerField\(.*validators=\[<UniqueValidator\(queryset=UniqueConstraintForeignKeyModel.objects.all\(\)\)>\]\)
765
+ class Meta:
766
+ validators = \[<UniqueTogetherValidator\(queryset=UniqueConstraintForeignKeyModel.objects.all\(\), fields=\('race_name', 'position'\), condition=<Q: \(AND: \('race_name', 'example'\)\)>\)>\]
767
+ """ )
768
+ assert re .search (expected , repr (serializer )) is not None
769
+
770
+ def test_unique_together_condition (self ):
771
+ """
772
+ Fields used in UniqueConstraint's condition must be included
773
+ into queryset existence check
774
+ """
775
+ fancy_model_condition_9 = FancyConditionModel .objects .create (id = 9 )
776
+ fancy_model_condition_10 = FancyConditionModel .objects .create (id = 10 )
777
+ fancy_model_condition_11 = FancyConditionModel .objects .create (id = 11 )
778
+ UniqueConstraintForeignKeyModel .objects .create (
779
+ race_name = 'condition' ,
780
+ position = 1 ,
781
+ global_id = 10 ,
782
+ fancy_conditions = fancy_model_condition_10 ,
783
+ )
784
+ serializer = UniqueConstraintForeignKeySerializer (data = {
785
+ 'race_name' : 'condition' ,
786
+ 'position' : 1 ,
787
+ 'global_id' : 11 ,
788
+ 'fancy_conditions' : fancy_model_condition_9 ,
789
+ })
790
+ assert serializer .is_valid ()
791
+ serializer = UniqueConstraintForeignKeySerializer (data = {
792
+ 'race_name' : 'condition' ,
793
+ 'position' : 1 ,
794
+ 'global_id' : 11 ,
795
+ 'fancy_conditions' : fancy_model_condition_11 ,
796
+ })
797
+ assert not serializer .is_valid ()
798
+
799
+ def test_unique_together_condition_fields_required (self ):
800
+ """
801
+ Fields used in UniqueConstraint's condition must be present in serializer
802
+ """
803
+ serializer = UniqueConstraintForeignKeySerializer (data = {
804
+ 'race_name' : 'condition' ,
805
+ 'position' : 1 ,
806
+ 'global_id' : 11 ,
807
+ })
808
+ assert not serializer .is_valid ()
809
+ assert serializer .errors == {'fancy_conditions' : ['This field is required.' ]}
810
+
811
+ class NoFieldsSerializer (serializers .ModelSerializer ):
812
+ class Meta :
813
+ model = UniqueConstraintForeignKeyModel
814
+ fields = ('race_name' , 'position' , 'global_id' )
815
+
816
+ serializer = NoFieldsSerializer ()
817
+ assert len (serializer .validators ) == 1
818
+
819
+ def test_single_field_uniq_validators (self ):
820
+ """
821
+ UniqueConstraint with single field must be transformed into
822
+ field's UniqueValidator
823
+ """
824
+ # Django 5 includes Max and Min values validators for IntegerField
825
+ extra_validators_qty = 2 if django_version [0 ] >= 5 else 0
826
+ serializer = UniqueConstraintForeignKeySerializer ()
827
+ assert len (serializer .validators ) == 2
828
+ validators = serializer .fields ['global_id' ].validators
829
+ assert len (validators ) == 1 + extra_validators_qty
830
+ assert validators [0 ].queryset == UniqueConstraintForeignKeyModel .objects
831
+
832
+ validators = serializer .fields ['fancy_conditions' ].validators
833
+ assert len (validators ) == 2 + extra_validators_qty
834
+ ids_in_qs = {frozenset (v .queryset .values_list (flat = True )) for v in validators if hasattr (v , "queryset" )}
835
+ assert ids_in_qs == {frozenset ([1 ]), frozenset ([3 ])}
836
+
837
+ def test_nullable_unique_constraint_fields_are_not_required (self ):
838
+ serializer = UniqueConstraintNullableSerializer (data = {'title' : 'Bob' })
839
+ self .assertTrue (serializer .is_valid (), serializer .errors )
840
+ result = serializer .save ()
841
+ self .assertIsInstance (result , UniqueConstraintNullableModel )
842
+
843
+
687
844
# Tests for `UniqueForDateValidator`
688
845
# ----------------------------------
689
846
0 commit comments