From b5c3cbe896972c34269c1015d469f9535ac50972 Mon Sep 17 00:00:00 2001 From: VimalN2005 Date: Sat, 12 Sep 2026 14:59:37 +0530 Subject: [PATCH 1/2] Fix unique_for_ validation on partial serializer updates (refs #6341) --- rest_framework/validators.py | 41 +++++++++++++-- tests/test_validators.py | 97 ++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 3 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 09debe2fcd..e9bd926a9d 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -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 `UniqueForValidator` 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] @@ -316,7 +320,38 @@ 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) diff --git a/tests/test_validators.py b/tests/test_validators.py index d75b9a139d..5954932b70 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1099,6 +1099,71 @@ 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(), @@ -1176,6 +1241,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` # ---------------------------------- @@ -1215,6 +1289,15 @@ 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') @@ -1256,6 +1339,20 @@ 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): From 7cc7949839ed7596bbbead5dae81a0cebe4c9309 Mon Sep 17 00:00:00 2001 From: VimalN2005 Date: Sat, 12 Sep 2026 18:40:04 +0530 Subject: [PATCH 2/2] Fix flake8 formatting in validators and tests --- rest_framework/validators.py | 1 - tests/test_validators.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index e9bd926a9d..0afaf42d0f 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -347,7 +347,6 @@ def __call__(self, attrs, serializer): (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 diff --git a/tests/test_validators.py b/tests/test_validators.py index 5954932b70..0941149639 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1163,7 +1163,6 @@ def test_partial_update_unchanged_values(self): ) assert serializer.is_valid(), serializer.errors - def test_many_update_requires_child_instance(self): serializer = UniqueForDateSerializer( instance=UniqueForDateModel.objects.all(), @@ -1298,7 +1297,6 @@ def test_partial_update_unique_for_year(self): assert serializer.is_valid(), serializer.errors - class HiddenFieldUniqueForDateModel(models.Model): slug = models.CharField(max_length=100, unique_for_date='published') published = models.DateTimeField(auto_now_add=True) @@ -1353,7 +1351,6 @@ class Meta: assert instance.slug == 'updated' - class ValidatorsTests(TestCase): def test_qs_exists_handles_type_error(self):