@@ -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.
@@ -740,12 +810,12 @@ class Meta:
740810 def test_single_field_uniq_validators (self ):
741811 """
742812 UniqueConstraint with single field must be transformed into
743- field's UniqueValidator
813+ field's UniqueValidator if no distinct condition fields exist (else UniqueTogetherValidator)
744814 """
745815 # Django 5 includes Max and Min values validators for IntegerField
746816 extra_validators_qty = 2 if django_version [0 ] >= 5 else 0
747817 serializer = UniqueConstraintSerializer ()
748- assert len (serializer .validators ) == 2
818+ assert len (serializer .validators ) == 4
749819 validators = serializer .fields ['global_id' ].validators
750820 assert len (validators ) == 1 + extra_validators_qty
751821 assert validators [0 ].queryset == UniqueConstraintModel .objects
0 commit comments