From 57968137fcd4d7246a831b7db2f527f9122bf4eb Mon Sep 17 00:00:00 2001 From: Max Morlocke Date: Fri, 25 Dec 2020 21:40:28 -0500 Subject: [PATCH] expose configurable allowing a default to be set on partial updates --- docs/api-guide/fields.md | 10 ++++++++-- rest_framework/fields.py | 12 +++++++++--- tests/test_serializer.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0492af9aa..0153025d2 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -48,7 +48,7 @@ Defaults to `True`. If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behaviour is to not populate the attribute at all. -The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned. +The `default` is not applied during partial update operations, unless `set_default_if_partial` is set to True. In the typical path partial update case only fields that are provided in the incoming data will have a validated value returned. May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `requires_context = True` attribute, then the serializer field will be passed as an argument. @@ -68,6 +68,12 @@ When serializing the instance, default will be used if the object attribute or d Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error. +### set_default_if_partial + +The `default` is not applied during partial update operations, unless `set_default_if_partial` is set to True. If set to True, the partial update will include the default value. Otherwise, partial updates will only apply to fields provided in the incoming data. + +Note: This parameter is particularly useful for HiddenInput's with default values. + ### `allow_null` Normally an error will be raised if `None` is passed to a serializer field. Set this keyword argument to `True` if `None` should be considered a valid value. @@ -542,7 +548,7 @@ For example, if `has_expired` was a property on the `Account` model, then the fo ## HiddenField -A field class that does not take a value based on user input, but instead takes its value from a default value or callable. +A field class that does not take a value based on user input, but instead takes its value from a default value or callable. In the case of a partial update, the `default` value will not be applied unless `set_default_on_partial` is set to True for this field. **Signature**: `HiddenField()` diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fdfba13f2..4397210bb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -307,6 +307,7 @@ MISSING_ERROR_MESSAGE = ( 'ValidationError raised by `{class_name}`, but error key `{key}` does ' 'not exist in the `error_messages` dictionary.' ) +NO_DEFAULT_SET_ON_PARTIAL = 'May not enforce set_default_on_partial without setting a default value' class Field: @@ -323,7 +324,7 @@ class Field: def __init__(self, read_only=False, write_only=False, required=None, default=empty, initial=empty, source=None, label=None, help_text=None, style=None, - error_messages=None, validators=None, allow_null=False): + error_messages=None, validators=None, allow_null=False, set_default_on_partial=False): self._creation_counter = Field._creation_counter Field._creation_counter += 1 @@ -336,6 +337,7 @@ class Field: assert not (read_only and required), NOT_READ_ONLY_REQUIRED assert not (required and default is not empty), NOT_REQUIRED_DEFAULT assert not (read_only and self.__class__ == Field), USE_READONLYFIELD + assert not (default is empty and set_default_on_partial), NO_DEFAULT_SET_ON_PARTIAL self.read_only = read_only self.write_only = write_only @@ -347,6 +349,7 @@ class Field: self.help_text = help_text self.style = {} if style is None else style self.allow_null = allow_null + self.set_default_on_partial = set_default_on_partial if self.default_empty_html is not empty: if default is not empty: @@ -498,8 +501,11 @@ class Field: raise `SkipField`, indicating that no value should be set in the validated data for this field. """ - if self.default is empty or getattr(self.root, 'partial', False): - # No default, or this is a partial update. + if self.default is empty or all([ + getattr(self.root, 'partial', False), + not getattr(self.root, "set_default_on_partial", False) + ]): + # No default, or this is a partial update where defaults are not set. raise SkipField() if callable(self.default): if hasattr(self.default, 'set_context'): diff --git a/tests/test_serializer.py b/tests/test_serializer.py index afefd70e1..5454ce3b7 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -610,6 +610,18 @@ class TestDefaultInclusions: assert serializer.validated_data == {'integer': 456} assert serializer.errors == {} + def test_default_should_be_included_on_partial_update_when_set_default_on_partial_true(self): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(default='abc', set_default_on_partial=True) + integer = serializers.IntegerField() + float = serializers.FloatField(required=False) + + instance = MockObject(char='def', integer=123, float=4.2) + serializer = ExampleSerializer(instance, data={'integer': 456}) + assert serializer.is_valid() + assert serializer.validated_data == {'char': 'abc', 'integer': 456} + assert serializer.errors == {} + class TestSerializerValidationWithCompiledRegexField: def setup(self):