Propagate 'default' from model_field to serializer field. (#9030)

* Propagate 'default' from model_field to serializer field

Fix #7469.

Co-authored-by: Nikhil Benesch <nikhil.benesch@gmail.com>

* 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 <john.parton.iv@gmail.com>
Co-authored-by: Nikhil Benesch <nikhil.benesch@gmail.com>
Co-authored-by: Rizwan Shaikh <rshaikh@ces-ltd.com>
Co-authored-by: Asif Saif Uddin <auvipy@gmail.com>
This commit is contained in:
rizwanshaikh 2023-08-07 12:05:54 +05:30 committed by GitHub
parent 589b5dca9e
commit 7a9db57eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 148 additions and 3 deletions

View File

@ -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. 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` ### `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. 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.

View File

@ -11,6 +11,7 @@ from django.http import Http404
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework import exceptions, serializers from rest_framework import exceptions, serializers
from rest_framework.fields import empty
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.utils.field_mapping import ClassLookupDict from rest_framework.utils.field_mapping import ClassLookupDict
@ -149,4 +150,7 @@ class SimpleMetadata(BaseMetadata):
for choice_value, choice_name in field.choices.items() 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 return field_info

View File

@ -9,6 +9,7 @@ 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 postgres_fields
from rest_framework.fields import empty
from rest_framework.validators import UniqueValidator from rest_framework.validators import UniqueValidator
NUMERIC_FIELD_TYPES = ( NUMERIC_FIELD_TYPES = (
@ -127,6 +128,9 @@ def get_field_kwargs(field_name, model_field):
kwargs['read_only'] = True kwargs['read_only'] = True
return kwargs 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: if model_field.has_default() or model_field.blank or model_field.null:
kwargs['required'] = False kwargs['required'] = False

View File

@ -184,6 +184,135 @@ class TestMetadata:
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
assert response.data == expected 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): def test_global_permissions(self):
""" """
If a user does not have global permissions on an action, then any If a user does not have global permissions on an action, then any

View File

@ -173,7 +173,7 @@ class TestRegularFieldMappings(TestCase):
TestSerializer(): TestSerializer():
auto_field = IntegerField(read_only=True) auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField() big_integer_field = IntegerField()
boolean_field = BooleanField(required=False) boolean_field = BooleanField(default=False, required=False)
char_field = CharField(max_length=100) char_field = CharField(max_length=100)
comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>]) comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
date_field = DateField() date_field = DateField()
@ -182,7 +182,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100) email_field = EmailField(max_length=100)
float_field = FloatField() float_field = FloatField()
integer_field = IntegerField() 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_integer_field = IntegerField()
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField()
slug_field = SlugField(allow_unicode=False, max_length=100) 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) length_limit_field = CharField(max_length=12, min_length=3)
blank_field = CharField(allow_blank=True, max_length=10, required=False) blank_field = CharField(allow_blank=True, max_length=10, required=False)
null_field = IntegerField(allow_null=True, 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') descriptive_field = IntegerField(help_text='Some help text', label='A label')
choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))
text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')))