mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-10-30 23:47:53 +03:00 
			
		
		
		
	Support violation_error_code and violation_error_message from UniqueConstraint in UniqueTogetherValidator (#9766)
				
					
				
			* fix(serializer): restore get_unique_together_constraints method signature Extracted error message logic to a separate method. fix: conditionally include violation_error_code for Django >= 5.0 fix(validators): use custom error message and code from model constraints * fix: add model parents to create unique_constraint_by_fields * fix: order of model classes in the unique_constraint_by_fields
This commit is contained in:
		
							parent
							
								
									a323cf7c0a
								
							
						
					
					
						commit
						9cf6efb4a8
					
				|  | @ -1569,6 +1569,17 @@ class ModelSerializer(Serializer): | |||
|             self.get_unique_for_date_validators() | ||||
|         ) | ||||
| 
 | ||||
|     def _get_constraint_violation_error_message(self, constraint): | ||||
|         """ | ||||
|         Returns the violation error message for the UniqueConstraint, | ||||
|         or None if the message is the default. | ||||
|         """ | ||||
|         violation_error_message = constraint.get_violation_error_message() | ||||
|         default_error_message = constraint.default_violation_error_message % {"name": constraint.name} | ||||
|         if violation_error_message == default_error_message: | ||||
|             return None | ||||
|         return violation_error_message | ||||
| 
 | ||||
|     def get_unique_together_validators(self): | ||||
|         """ | ||||
|         Determine a default set of validators for any unique_together constraints. | ||||
|  | @ -1595,6 +1606,13 @@ class ModelSerializer(Serializer): | |||
|         for name, source in field_sources.items(): | ||||
|             source_map[source].append(name) | ||||
| 
 | ||||
|         unique_constraint_by_fields = { | ||||
|             constraint.fields: constraint | ||||
|             for model_cls in (*self.Meta.model._meta.parents, self.Meta.model) | ||||
|             for constraint in model_cls._meta.constraints | ||||
|             if isinstance(constraint, models.UniqueConstraint) | ||||
|         } | ||||
| 
 | ||||
|         # Note that we make sure to check `unique_together` both on the | ||||
|         # base model class, but also on any parent classes. | ||||
|         validators = [] | ||||
|  | @ -1621,11 +1639,17 @@ class ModelSerializer(Serializer): | |||
|                 ) | ||||
| 
 | ||||
|             field_names = tuple(source_map[f][0] for f in unique_together) | ||||
| 
 | ||||
|             constraint = unique_constraint_by_fields.get(tuple(unique_together)) | ||||
|             violation_error_message = self._get_constraint_violation_error_message(constraint) if constraint else None | ||||
| 
 | ||||
|             validator = UniqueTogetherValidator( | ||||
|                 queryset=queryset, | ||||
|                 fields=field_names, | ||||
|                 condition_fields=tuple(source_map[f][0] for f in condition_fields), | ||||
|                 condition=condition, | ||||
|                 message=violation_error_message, | ||||
|                 code=getattr(constraint, 'violation_error_code', None), | ||||
|             ) | ||||
|             validators.append(validator) | ||||
|         return validators | ||||
|  |  | |||
|  | @ -111,13 +111,15 @@ class UniqueTogetherValidator: | |||
|     message = _('The fields {field_names} must make a unique set.') | ||||
|     missing_message = _('This field is required.') | ||||
|     requires_context = True | ||||
|     code = 'unique' | ||||
| 
 | ||||
|     def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None): | ||||
|     def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None, code=None): | ||||
|         self.queryset = queryset | ||||
|         self.fields = fields | ||||
|         self.message = message or self.message | ||||
|         self.condition_fields = [] if condition_fields is None else condition_fields | ||||
|         self.condition = condition | ||||
|         self.code = code or self.code | ||||
| 
 | ||||
|     def enforce_required_fields(self, attrs, serializer): | ||||
|         """ | ||||
|  | @ -198,7 +200,7 @@ class UniqueTogetherValidator: | |||
|         if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs): | ||||
|             field_names = ', '.join(self.fields) | ||||
|             message = self.message.format(field_names=field_names) | ||||
|             raise ValidationError(message, code='unique') | ||||
|             raise ValidationError(message, code=self.code) | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return '<{}({})>'.format( | ||||
|  | @ -217,6 +219,7 @@ class UniqueTogetherValidator: | |||
|                 and self.missing_message == other.missing_message | ||||
|                 and self.queryset == other.queryset | ||||
|                 and self.fields == other.fields | ||||
|                 and self.code == other.code | ||||
|                 ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -616,6 +616,26 @@ class UniqueConstraintNullableModel(models.Model): | |||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| class UniqueConstraintCustomMessageCodeModel(models.Model): | ||||
|     username = models.CharField(max_length=32) | ||||
|     company_id = models.IntegerField() | ||||
|     role = models.CharField(max_length=32) | ||||
| 
 | ||||
|     class Meta: | ||||
|         constraints = [ | ||||
|             models.UniqueConstraint( | ||||
|                 fields=("username", "company_id"), | ||||
|                 name="unique_username_company_custom_msg", | ||||
|                 violation_error_message="Username must be unique within a company.", | ||||
|                 **(dict(violation_error_code="duplicate_username") if django_version[0] >= 5 else {}), | ||||
|             ), | ||||
|             models.UniqueConstraint( | ||||
|                 fields=("company_id", "role"), | ||||
|                 name="unique_company_role_default_msg", | ||||
|             ), | ||||
|         ] | ||||
| 
 | ||||
| 
 | ||||
| class UniqueConstraintSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = UniqueConstraintModel | ||||
|  | @ -628,6 +648,12 @@ class UniqueConstraintNullableSerializer(serializers.ModelSerializer): | |||
|         fields = ('title', 'age', 'tag') | ||||
| 
 | ||||
| 
 | ||||
| class UniqueConstraintCustomMessageCodeSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = UniqueConstraintCustomMessageCodeModel | ||||
|         fields = ('username', 'company_id', 'role') | ||||
| 
 | ||||
| 
 | ||||
| class TestUniqueConstraintValidation(TestCase): | ||||
|     def setUp(self): | ||||
|         self.instance = UniqueConstraintModel.objects.create( | ||||
|  | @ -778,6 +804,31 @@ class TestUniqueConstraintValidation(TestCase): | |||
|         ) | ||||
|         assert serializer.is_valid() | ||||
| 
 | ||||
|     def test_unique_constraint_custom_message_code(self): | ||||
|         UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member") | ||||
|         expected_code = "duplicate_username" if django_version[0] >= 5 else UniqueTogetherValidator.code | ||||
| 
 | ||||
|         serializer = UniqueConstraintCustomMessageCodeSerializer(data={ | ||||
|             "username": "Alice", | ||||
|             "company_id": 1, | ||||
|             "role": "admin", | ||||
|         }) | ||||
|         assert not serializer.is_valid() | ||||
|         assert serializer.errors == {"non_field_errors": ["Username must be unique within a company."]} | ||||
|         assert serializer.errors["non_field_errors"][0].code == expected_code | ||||
| 
 | ||||
|     def test_unique_constraint_default_message_code(self): | ||||
|         UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member") | ||||
|         serializer = UniqueConstraintCustomMessageCodeSerializer(data={ | ||||
|             "username": "John", | ||||
|             "company_id": 1, | ||||
|             "role": "member", | ||||
|         }) | ||||
|         expected_message = UniqueTogetherValidator.message.format(field_names=', '.join(("company_id", "role"))) | ||||
|         assert not serializer.is_valid() | ||||
|         assert serializer.errors == {"non_field_errors": [expected_message]} | ||||
|         assert serializer.errors["non_field_errors"][0].code == UniqueTogetherValidator.code | ||||
| 
 | ||||
| 
 | ||||
| # Tests for `UniqueForDateValidator` | ||||
| # ---------------------------------- | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user