@@ -170,6 +170,24 @@ class Meta:
170170 unique_together = ('race_name' , 'position' )
171171
172172
173+ class ConditionUniquenessTogetherModel (models .Model ):
174+ """
175+ Used to ensure that unique constraints with single fields but at least one other
176+ distinct condition field are included when checking unique_together constraints.
177+ """
178+ race_name = models .CharField (max_length = 100 )
179+ position = models .IntegerField ()
180+
181+ class Meta :
182+ constraints = [
183+ models .UniqueConstraint (
184+ name = "condition_uniqueness_together_model_race_name" ,
185+ fields = ('race_name' ,),
186+ condition = models .Q (position__lte = 1 )
187+ )
188+ ]
189+
190+
173191class UniquenessTogetherSerializer (serializers .ModelSerializer ):
174192 class Meta :
175193 model = UniquenessTogetherModel
@@ -182,6 +200,12 @@ class Meta:
182200 fields = '__all__'
183201
184202
203+ class ConditionUniquenessTogetherSerializer (serializers .ModelSerializer ):
204+ class Meta :
205+ model = ConditionUniquenessTogetherModel
206+ fields = '__all__'
207+
208+
185209class TestUniquenessTogetherValidation (TestCase ):
186210 def setUp (self ):
187211 self .instance = UniquenessTogetherModel .objects .create (
@@ -222,6 +246,22 @@ def test_is_not_unique_together(self):
222246 ]
223247 }
224248
249+ def test_is_not_unique_together_condition_based (self ):
250+ """
251+ Failing unique together validation should result in non field errors when a condition-based
252+ unique together constraint is violated.
253+ """
254+ ConditionUniquenessTogetherModel .objects .create (race_name = 'example' , position = 1 )
255+
256+ data = {'race_name' : 'example' , 'position' : 1 }
257+ serializer = ConditionUniquenessTogetherSerializer (data = data )
258+ assert not serializer .is_valid ()
259+ assert serializer .errors == {
260+ 'non_field_errors' : [
261+ 'The fields race_name must make a unique set.'
262+ ]
263+ }
264+
225265 def test_is_unique_together (self ):
226266 """
227267 In a unique together validation, one field may be non-unique
@@ -235,6 +275,21 @@ def test_is_unique_together(self):
235275 'position' : 2
236276 }
237277
278+ def test_unique_together_condition_based (self ):
279+ """
280+ In a unique together validation, one field may be non-unique
281+ so long as the set as a whole is unique.
282+ """
283+ ConditionUniquenessTogetherModel .objects .create (race_name = 'example' , position = 1 )
284+
285+ data = {'race_name' : 'other' , 'position' : 1 }
286+ serializer = ConditionUniquenessTogetherSerializer (data = data )
287+ assert serializer .is_valid ()
288+ assert serializer .validated_data == {
289+ 'race_name' : 'other' ,
290+ 'position' : 1
291+ }
292+
238293 def test_updated_instance_excluded_from_unique_together (self ):
239294 """
240295 When performing an update, the existing instance does not count
@@ -248,6 +303,21 @@ def test_updated_instance_excluded_from_unique_together(self):
248303 'position' : 1
249304 }
250305
306+ def test_updated_instance_excluded_from_unique_together_condition_based (self ):
307+ """
308+ When performing an update, the existing instance does not count
309+ as a match against uniqueness.
310+ """
311+ ConditionUniquenessTogetherModel .objects .create (race_name = 'example' , position = 1 )
312+
313+ data = {'race_name' : 'example' , 'position' : 0 }
314+ serializer = ConditionUniquenessTogetherSerializer (self .instance , data = data )
315+ assert serializer .is_valid ()
316+ assert serializer .validated_data == {
317+ 'race_name' : 'example' ,
318+ 'position' : 0
319+ }
320+
251321 def test_unique_together_is_required (self ):
252322 """
253323 In a unique together validation, all fields are required.
@@ -699,20 +769,17 @@ class Meta:
699769 def test_single_field_uniq_validators (self ):
700770 """
701771 UniqueConstraint with single field must be transformed into
702- field's UniqueValidator
772+ field's UniqueValidator if no distinct condition fields exist (else UniqueTogetherValidator)
703773 """
704774 # Django 5 includes Max and Min values validators for IntegerField
705775 extra_validators_qty = 2 if django_version [0 ] >= 5 else 0
706776 serializer = UniqueConstraintSerializer ()
707- assert len (serializer .validators ) == 2
777+ assert len (serializer .validators ) == 4
708778 validators = serializer .fields ['global_id' ].validators
709779 assert len (validators ) == 1 + extra_validators_qty
710780 assert validators [0 ].queryset == UniqueConstraintModel .objects
711-
712- validators = serializer .fields ['fancy_conditions' ].validators
713- assert len (validators ) == 2 + extra_validators_qty
714781 ids_in_qs = {frozenset (v .queryset .values_list (flat = True )) for v in validators if hasattr (v , "queryset" )}
715- assert ids_in_qs == {frozenset ([1 ]), frozenset ([ 3 ])}
782+ assert ids_in_qs == {frozenset ([1 , 2 , 3 ])}
716783
717784 def test_nullable_unique_constraint_fields_are_not_required (self ):
718785 serializer = UniqueConstraintNullableSerializer (data = {'title' : 'Bob' })
0 commit comments