Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions rest_framework/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,15 @@ def __init__(self, queryset, field, date_field, message=None):
self.date_field = date_field
self.message = message or self.message

def enforce_required_fields(self, attrs):
def enforce_required_fields(self, attrs, serializer=None):
"""
The `UniqueFor<Range>Validator` classes always force an implied
'required' state on the fields they are applied to.
'required' state on the fields they are applied to during creates.
On partial updates, missing fields are bypassed.
"""
if serializer is not None and serializer.instance is not None:
return

missing_items = {
field_name: self.missing_message
for field_name in [self.field, self.date_field]
Expand Down Expand Up @@ -316,7 +320,37 @@ def __call__(self, attrs, serializer):
date_field_name = serializer.fields[self.date_field].source_attrs[-1]

_check_single_instance(serializer, serializer.instance, self)
self.enforce_required_fields(attrs)
self.enforce_required_fields(attrs, serializer)

if serializer.instance is not None:
# On update: if neither field is present in attrs, skip validation
if self.field not in attrs and self.date_field not in attrs:
return

# If only one field is provided, resolve the other from the existing instance
attrs = attrs.copy()
if self.field not in attrs:
attrs[self.field] = getattr(serializer.instance, field_name)
if self.date_field not in attrs:
date_val = getattr(serializer.instance, date_field_name)
if date_val is not None and isinstance(date_val, str):
try:
date_val = serializer.fields[self.date_field].to_internal_value(date_val)
except Exception:
pass
attrs[self.date_field] = date_val

# If both fields are unchanged on the instance, skip validation
instance_date = getattr(serializer.instance, date_field_name)
if (attrs[self.field] == getattr(serializer.instance, field_name) and
(attrs[self.date_field] == instance_date or
(isinstance(instance_date, str) and str(attrs[self.date_field]) == instance_date))):
return

# If date_field is None, skip validation
if attrs.get(self.date_field) is None:
return

queryset = self.queryset
queryset = self.filter_queryset(attrs, queryset, field_name, date_field_name)
queryset = self.exclude_current_instance(attrs, queryset, serializer.instance)
Expand Down
94 changes: 94 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,70 @@ def test_updated_instance_excluded_from_unique_for_date(self):
'published': datetime.date(2000, 1, 1)
}

def test_partial_update_without_fields(self):
"""
A partial update that changes neither the field nor the date field
should not raise validation errors.
"""
serializer = UniqueForDateSerializer(instance=self.instance, data={}, partial=True)
assert serializer.is_valid(), serializer.errors

def test_partial_update_single_field_unique(self):
"""
A partial update changing only the slug should resolve the date
from the instance and succeed if unique.
"""
serializer = UniqueForDateSerializer(
instance=self.instance,
data={'slug': 'brand-new-slug'},
partial=True
)
assert serializer.is_valid(), serializer.errors
assert serializer.validated_data['slug'] == 'brand-new-slug'

def test_partial_update_single_field_conflict(self):
"""
A partial update changing only the slug should conflict if another
record already exists with that slug on the same date.
"""
UniqueForDateModel.objects.create(slug='other-slug', published='2000-01-01')
serializer = UniqueForDateSerializer(
instance=self.instance,
data={'slug': 'other-slug'},
partial=True
)
assert not serializer.is_valid()
assert serializer.errors == {
'slug': ['This field must be unique for the "published" date.']
}

def test_partial_update_date_field_conflict(self):
"""
A partial update changing only the date field should conflict if another
record already exists with that slug on the new date.
"""
UniqueForDateModel.objects.create(slug='existing', published='2000-01-02')
serializer = UniqueForDateSerializer(
instance=self.instance,
data={'published': '2000-01-02'},
partial=True
)
assert not serializer.is_valid()
assert serializer.errors == {
'slug': ['This field must be unique for the "published" date.']
}

def test_partial_update_unchanged_values(self):
"""
A partial update that submits the same values should not fail uniqueness.
"""
serializer = UniqueForDateSerializer(
instance=self.instance,
data={'slug': 'existing', 'published': '2000-01-01'},
partial=True
)
assert serializer.is_valid(), serializer.errors

def test_many_update_requires_child_instance(self):
serializer = UniqueForDateSerializer(
instance=UniqueForDateModel.objects.all(),
Expand Down Expand Up @@ -1176,6 +1240,15 @@ def test_unique_for_month(self):
'published': datetime.date(2017, 2, 1)
}

def test_partial_update_unique_for_month(self):
serializer = UniqueForMonthSerializer(
instance=self.instance,
data={'slug': 'updated'},
partial=True
)
assert serializer.is_valid(), serializer.errors


# Tests for `UniqueForYearValidator`
# ----------------------------------

Expand Down Expand Up @@ -1215,6 +1288,14 @@ def test_unique_for_year(self):
'published': datetime.date(2018, 1, 1)
}

def test_partial_update_unique_for_year(self):
serializer = UniqueForYearSerializer(
instance=self.instance,
data={'slug': 'updated'},
partial=True
)
assert serializer.is_valid(), serializer.errors


class HiddenFieldUniqueForDateModel(models.Model):
slug = models.CharField(max_length=100, unique_for_date='published')
Expand Down Expand Up @@ -1256,6 +1337,19 @@ class Meta:
""")
assert repr(serializer) == expected

def test_hidden_field_partial_update(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = HiddenFieldUniqueForDateModel
fields = ('id', 'slug')

instance = HiddenFieldUniqueForDateModel.objects.create(slug='initial')
serializer = TestSerializer(instance=instance, data={'slug': 'updated'}, partial=True)
assert serializer.is_valid(), serializer.errors
serializer.save()
instance.refresh_from_db()
assert instance.slug == 'updated'


class ValidatorsTests(TestCase):

Expand Down