@@ -552,6 +552,45 @@ class Meta:
552552 ]
553553
554554
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+
555594class UniqueConstraintNullableModel (models .Model ):
556595 title = models .CharField (max_length = 100 )
557596 age = models .IntegerField (null = True )
@@ -570,6 +609,12 @@ class Meta:
570609 fields = '__all__'
571610
572611
612+ class UniqueConstraintForeignKeySerializer (serializers .ModelSerializer ):
613+ class Meta :
614+ model = UniqueConstraintForeignKeyModel
615+ fields = '__all__'
616+
617+
573618class UniqueConstraintNullableSerializer (serializers .ModelSerializer ):
574619 class Meta :
575620 model = UniqueConstraintNullableModel
@@ -684,6 +729,118 @@ def test_nullable_unique_constraint_fields_are_not_required(self):
684729 self .assertIsInstance (result , UniqueConstraintNullableModel )
685730
686731
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+
687844# Tests for `UniqueForDateValidator`
688845# ----------------------------------
689846
0 commit comments