From 7a9db57eaf829367601fbaac5f1811d1fcb712fb Mon Sep 17 00:00:00 2001 From: rizwanshaikh Date: Mon, 7 Aug 2023 12:05:54 +0530 Subject: [PATCH] Propagate 'default' from model_field to serializer field. (#9030) * Propagate 'default' from model_field to serializer field Fix #7469. Co-authored-by: Nikhil Benesch * updated field default on serializer according to openapi generation and added that to options action response * added notes regarding default value propagation from model to serializer field * updated note * Update docs/api-guide/fields.md * Update docs/api-guide/fields.md * Update docs/api-guide/fields.md * Update docs/api-guide/fields.md * Update docs/api-guide/fields.md * Update docs/api-guide/fields.md --------- Co-authored-by: John Parton Co-authored-by: Nikhil Benesch Co-authored-by: Rizwan Shaikh Co-authored-by: Asif Saif Uddin --- docs/api-guide/fields.md | 8 ++ rest_framework/metadata.py | 4 + rest_framework/utils/field_mapping.py | 4 + tests/test_metadata.py | 129 ++++++++++++++++++++++++++ tests/test_model_serializer.py | 6 +- 5 files changed, 148 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4ac3e3c31..7c2716218 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -68,6 +68,14 @@ 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. +Notes regarding default value propagation from model to serializer: + +All the default values from model will pass as default to the serializer and the options method. + +If the default is callable then it will be propagated to & evaluated every time in the serializer but not in options method. + +If the value for given field is not given then default value will be present in the serializer and available in serializer's methods. Specified validation on given field will be evaluated on default value as that field will be present in the serializer. + ### `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. diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index 364ca5b14..fd0f4e163 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -11,6 +11,7 @@ from django.http import Http404 from django.utils.encoding import force_str from rest_framework import exceptions, serializers +from rest_framework.fields import empty from rest_framework.request import clone_request from rest_framework.utils.field_mapping import ClassLookupDict @@ -149,4 +150,7 @@ class SimpleMetadata(BaseMetadata): for choice_value, choice_name in field.choices.items() ] + if getattr(field, 'default', None) and field.default != empty and not callable(field.default): + field_info['default'] = field.default + return field_info diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index fc63f96fe..30bb65e0c 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -9,6 +9,7 @@ from django.db import models from django.utils.text import capfirst from rest_framework.compat import postgres_fields +from rest_framework.fields import empty from rest_framework.validators import UniqueValidator NUMERIC_FIELD_TYPES = ( @@ -127,6 +128,9 @@ def get_field_kwargs(field_name, model_field): kwargs['read_only'] = True return kwargs + if model_field.default is not None and model_field.default != empty and not callable(model_field.default): + kwargs['default'] = model_field.default + if model_field.has_default() or model_field.blank or model_field.null: kwargs['required'] = False diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 1bdc8697c..387b9ecde 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -184,6 +184,135 @@ class TestMetadata: assert response.status_code == status.HTTP_200_OK assert response.data == expected + def test_actions_with_default(self): + """ + On generic views OPTIONS should return an 'actions' key with metadata + on the fields with default that may be supplied to PUT and POST requests. + """ + class NestedField(serializers.Serializer): + a = serializers.IntegerField(default=2) + b = serializers.IntegerField() + + class ExampleSerializer(serializers.Serializer): + choice_field = serializers.ChoiceField(['red', 'green', 'blue'], default='red') + integer_field = serializers.IntegerField( + min_value=1, max_value=1000, default=1 + ) + char_field = serializers.CharField( + min_length=3, max_length=40, default="example" + ) + list_field = serializers.ListField( + child=serializers.ListField( + child=serializers.IntegerField(default=1) + ) + ) + nested_field = NestedField() + uuid_field = serializers.UUIDField(label="UUID field") + + class ExampleView(views.APIView): + """Example view.""" + def post(self, request): + pass + + def get_serializer(self): + return ExampleSerializer() + + view = ExampleView.as_view() + response = view(request=request) + expected = { + 'name': 'Example', + 'description': 'Example view.', + 'renders': [ + 'application/json', + 'text/html' + ], + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'actions': { + 'POST': { + 'choice_field': { + 'type': 'choice', + 'required': False, + 'read_only': False, + 'label': 'Choice field', + "choices": [ + {'value': 'red', 'display_name': 'red'}, + {'value': 'green', 'display_name': 'green'}, + {'value': 'blue', 'display_name': 'blue'} + ], + 'default': 'red' + }, + 'integer_field': { + 'type': 'integer', + 'required': False, + 'read_only': False, + 'label': 'Integer field', + 'min_value': 1, + 'max_value': 1000, + 'default': 1 + }, + 'char_field': { + 'type': 'string', + 'required': False, + 'read_only': False, + 'label': 'Char field', + 'min_length': 3, + 'max_length': 40, + 'default': 'example' + }, + 'list_field': { + 'type': 'list', + 'required': True, + 'read_only': False, + 'label': 'List field', + 'child': { + 'type': 'list', + 'required': True, + 'read_only': False, + 'child': { + 'type': 'integer', + 'required': False, + 'read_only': False, + 'default': 1 + } + } + }, + 'nested_field': { + 'type': 'nested object', + 'required': True, + 'read_only': False, + 'label': 'Nested field', + 'children': { + 'a': { + 'type': 'integer', + 'required': False, + 'read_only': False, + 'label': 'A', + 'default': 2 + }, + 'b': { + 'type': 'integer', + 'required': True, + 'read_only': False, + 'label': 'B' + } + } + }, + 'uuid_field': { + 'type': 'string', + 'required': True, + 'read_only': False, + 'label': 'UUID field' + } + } + } + } + assert response.status_code == status.HTTP_200_OK + assert response.data == expected + def test_global_permissions(self): """ If a user does not have global permissions on an action, then any diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index e2d4bbc30..20d0319fc 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -173,7 +173,7 @@ class TestRegularFieldMappings(TestCase): TestSerializer(): auto_field = IntegerField(read_only=True) big_integer_field = IntegerField() - boolean_field = BooleanField(required=False) + boolean_field = BooleanField(default=False, required=False) char_field = CharField(max_length=100) comma_separated_integer_field = CharField(max_length=100, validators=[]) date_field = DateField() @@ -182,7 +182,7 @@ class TestRegularFieldMappings(TestCase): email_field = EmailField(max_length=100) float_field = FloatField() integer_field = IntegerField() - null_boolean_field = BooleanField(allow_null=True, required=False) + null_boolean_field = BooleanField(allow_null=True, default=False, required=False) positive_integer_field = IntegerField() positive_small_integer_field = IntegerField() slug_field = SlugField(allow_unicode=False, max_length=100) @@ -210,7 +210,7 @@ class TestRegularFieldMappings(TestCase): length_limit_field = CharField(max_length=12, min_length=3) blank_field = CharField(allow_blank=True, max_length=10, required=False) null_field = IntegerField(allow_null=True, required=False) - default_field = IntegerField(required=False) + default_field = IntegerField(default=0, required=False) descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))