mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-13 16:54:47 +03:00
chore: unique constraints with distinct condition fields use unique together validator
This commit is contained in:
parent
2ae8c117da
commit
6665e71e82
|
@ -1435,12 +1435,22 @@ class ModelSerializer(Serializer):
|
||||||
for unique_together in parent_class._meta.unique_together:
|
for unique_together in parent_class._meta.unique_together:
|
||||||
yield unique_together, model._default_manager, [], None
|
yield unique_together, model._default_manager, [], None
|
||||||
for constraint in parent_class._meta.constraints:
|
for constraint in parent_class._meta.constraints:
|
||||||
if isinstance(constraint, models.UniqueConstraint) and len(constraint.fields) > 1:
|
if isinstance(constraint, models.UniqueConstraint):
|
||||||
if constraint.condition is None:
|
if constraint.condition is None:
|
||||||
condition_fields = []
|
condition_fields = []
|
||||||
else:
|
else:
|
||||||
condition_fields = list(get_referenced_base_fields_from_q(constraint.condition))
|
condition_fields = list(
|
||||||
yield (constraint.fields, model._default_manager, condition_fields, constraint.condition)
|
get_referenced_base_fields_from_q(constraint.condition)
|
||||||
|
)
|
||||||
|
|
||||||
|
required_fields = {*constraint.fields, *condition_fields}
|
||||||
|
if len(required_fields) > 1:
|
||||||
|
yield (
|
||||||
|
constraint.fields,
|
||||||
|
model._default_manager,
|
||||||
|
condition_fields,
|
||||||
|
constraint.condition,
|
||||||
|
)
|
||||||
|
|
||||||
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
|
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -8,7 +8,9 @@ from django.core import validators
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
|
||||||
from rest_framework.compat import postgres_fields
|
from rest_framework.compat import (
|
||||||
|
get_referenced_base_fields_from_q, postgres_fields
|
||||||
|
)
|
||||||
from rest_framework.validators import UniqueValidator
|
from rest_framework.validators import UniqueValidator
|
||||||
|
|
||||||
NUMERIC_FIELD_TYPES = (
|
NUMERIC_FIELD_TYPES = (
|
||||||
|
@ -79,10 +81,16 @@ def get_unique_validators(field_name, model_field):
|
||||||
unique_error_message = get_unique_error_message(model_field)
|
unique_error_message = get_unique_error_message(model_field)
|
||||||
queryset = model_field.model._default_manager
|
queryset = model_field.model._default_manager
|
||||||
for condition in conditions:
|
for condition in conditions:
|
||||||
yield UniqueValidator(
|
condition_fields = (
|
||||||
queryset=queryset if condition is None else queryset.filter(condition),
|
get_referenced_base_fields_from_q(condition)
|
||||||
message=unique_error_message
|
if condition is not None
|
||||||
|
else set()
|
||||||
)
|
)
|
||||||
|
if len(field_set | condition_fields) == 1:
|
||||||
|
yield UniqueValidator(
|
||||||
|
queryset=queryset if condition is None else queryset.filter(condition),
|
||||||
|
message=unique_error_message,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_field_kwargs(field_name, model_field):
|
def get_field_kwargs(field_name, model_field):
|
||||||
|
|
|
@ -170,6 +170,24 @@ class NullUniquenessTogetherModel(models.Model):
|
||||||
unique_together = ('race_name', 'position')
|
unique_together = ('race_name', 'position')
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionUniquenessTogetherModel(models.Model):
|
||||||
|
"""
|
||||||
|
Used to ensure that unique constraints with single fields but at least one other
|
||||||
|
distinct condition field are included when checking unique_together constraints.
|
||||||
|
"""
|
||||||
|
race_name = models.CharField(max_length=100)
|
||||||
|
position = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
name="condition_uniqueness_together_model_race_name",
|
||||||
|
fields=('race_name',),
|
||||||
|
condition=models.Q(position__lte=1)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class UniquenessTogetherSerializer(serializers.ModelSerializer):
|
class UniquenessTogetherSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = UniquenessTogetherModel
|
model = UniquenessTogetherModel
|
||||||
|
@ -182,6 +200,12 @@ class NullUniquenessTogetherSerializer(serializers.ModelSerializer):
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionUniquenessTogetherSerializer(serializers.ModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = ConditionUniquenessTogetherModel
|
||||||
|
fields = '__all__'
|
||||||
|
|
||||||
|
|
||||||
class TestUniquenessTogetherValidation(TestCase):
|
class TestUniquenessTogetherValidation(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.instance = UniquenessTogetherModel.objects.create(
|
self.instance = UniquenessTogetherModel.objects.create(
|
||||||
|
@ -222,6 +246,22 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_is_not_unique_together_condition_based(self):
|
||||||
|
"""
|
||||||
|
Failing unique together validation should result in non field errors when a condition-based
|
||||||
|
unique together constraint is violated.
|
||||||
|
"""
|
||||||
|
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
|
||||||
|
|
||||||
|
data = {'race_name': 'example', 'position': 1}
|
||||||
|
serializer = ConditionUniquenessTogetherSerializer(data=data)
|
||||||
|
assert not serializer.is_valid()
|
||||||
|
assert serializer.errors == {
|
||||||
|
'non_field_errors': [
|
||||||
|
'The fields race_name must make a unique set.'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
def test_is_unique_together(self):
|
def test_is_unique_together(self):
|
||||||
"""
|
"""
|
||||||
In a unique together validation, one field may be non-unique
|
In a unique together validation, one field may be non-unique
|
||||||
|
@ -235,6 +275,21 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
'position': 2
|
'position': 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_unique_together_condition_based(self):
|
||||||
|
"""
|
||||||
|
In a unique together validation, one field may be non-unique
|
||||||
|
so long as the set as a whole is unique.
|
||||||
|
"""
|
||||||
|
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
|
||||||
|
|
||||||
|
data = {'race_name': 'other', 'position': 1}
|
||||||
|
serializer = ConditionUniquenessTogetherSerializer(data=data)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert serializer.validated_data == {
|
||||||
|
'race_name': 'other',
|
||||||
|
'position': 1
|
||||||
|
}
|
||||||
|
|
||||||
def test_updated_instance_excluded_from_unique_together(self):
|
def test_updated_instance_excluded_from_unique_together(self):
|
||||||
"""
|
"""
|
||||||
When performing an update, the existing instance does not count
|
When performing an update, the existing instance does not count
|
||||||
|
@ -248,6 +303,21 @@ class TestUniquenessTogetherValidation(TestCase):
|
||||||
'position': 1
|
'position': 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_updated_instance_excluded_from_unique_together_condition_based(self):
|
||||||
|
"""
|
||||||
|
When performing an update, the existing instance does not count
|
||||||
|
as a match against uniqueness.
|
||||||
|
"""
|
||||||
|
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
|
||||||
|
|
||||||
|
data = {'race_name': 'example', 'position': 0}
|
||||||
|
serializer = ConditionUniquenessTogetherSerializer(self.instance, data=data)
|
||||||
|
assert serializer.is_valid()
|
||||||
|
assert serializer.validated_data == {
|
||||||
|
'race_name': 'example',
|
||||||
|
'position': 0
|
||||||
|
}
|
||||||
|
|
||||||
def test_unique_together_is_required(self):
|
def test_unique_together_is_required(self):
|
||||||
"""
|
"""
|
||||||
In a unique together validation, all fields are required.
|
In a unique together validation, all fields are required.
|
||||||
|
@ -699,20 +769,17 @@ class TestUniqueConstraintValidation(TestCase):
|
||||||
def test_single_field_uniq_validators(self):
|
def test_single_field_uniq_validators(self):
|
||||||
"""
|
"""
|
||||||
UniqueConstraint with single field must be transformed into
|
UniqueConstraint with single field must be transformed into
|
||||||
field's UniqueValidator
|
field's UniqueValidator if no distinct condition fields exist (else UniqueTogetherValidator)
|
||||||
"""
|
"""
|
||||||
# Django 5 includes Max and Min values validators for IntegerField
|
# Django 5 includes Max and Min values validators for IntegerField
|
||||||
extra_validators_qty = 2 if django_version[0] >= 5 else 0
|
extra_validators_qty = 2 if django_version[0] >= 5 else 0
|
||||||
serializer = UniqueConstraintSerializer()
|
serializer = UniqueConstraintSerializer()
|
||||||
assert len(serializer.validators) == 2
|
assert len(serializer.validators) == 4
|
||||||
validators = serializer.fields['global_id'].validators
|
validators = serializer.fields['global_id'].validators
|
||||||
assert len(validators) == 1 + extra_validators_qty
|
assert len(validators) == 1 + extra_validators_qty
|
||||||
assert validators[0].queryset == UniqueConstraintModel.objects
|
assert validators[0].queryset == UniqueConstraintModel.objects
|
||||||
|
|
||||||
validators = serializer.fields['fancy_conditions'].validators
|
|
||||||
assert len(validators) == 2 + extra_validators_qty
|
|
||||||
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
|
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
|
||||||
assert ids_in_qs == {frozenset([1]), frozenset([3])}
|
assert ids_in_qs == {frozenset([1, 2, 3])}
|
||||||
|
|
||||||
def test_nullable_unique_constraint_fields_are_not_required(self):
|
def test_nullable_unique_constraint_fields_are_not_required(self):
|
||||||
serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
|
serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
|
||||||
|
|
Loading…
Reference in New Issue
Block a user