mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
7a9db57eaf
* 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>
543 lines
20 KiB
Python
543 lines
20 KiB
Python
import pytest
|
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
from django.db import models
|
|
from django.test import TestCase
|
|
|
|
from rest_framework import (
|
|
exceptions, metadata, serializers, status, versioning, views
|
|
)
|
|
from rest_framework.renderers import BrowsableAPIRenderer
|
|
from rest_framework.test import APIRequestFactory
|
|
|
|
from .models import BasicModel
|
|
|
|
request = APIRequestFactory().options('/')
|
|
|
|
|
|
class TestMetadata:
|
|
|
|
def test_determine_metadata_abstract_method_raises_proper_error(self):
|
|
with pytest.raises(NotImplementedError):
|
|
metadata.BaseMetadata().determine_metadata(None, None)
|
|
|
|
def test_metadata(self):
|
|
"""
|
|
OPTIONS requests to views should return a valid 200 response.
|
|
"""
|
|
class ExampleView(views.APIView):
|
|
"""Example view."""
|
|
pass
|
|
|
|
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'
|
|
]
|
|
}
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.data == expected
|
|
|
|
def test_none_metadata(self):
|
|
"""
|
|
OPTIONS requests to views where `metadata_class = None` should raise
|
|
a MethodNotAllowed exception, which will result in an HTTP 405 response.
|
|
"""
|
|
class ExampleView(views.APIView):
|
|
metadata_class = None
|
|
|
|
view = ExampleView.as_view()
|
|
response = view(request=request)
|
|
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
|
|
assert response.data == {'detail': 'Method "OPTIONS" not allowed.'}
|
|
|
|
def test_actions(self):
|
|
"""
|
|
On generic views OPTIONS should return an 'actions' key with metadata
|
|
on the fields that may be supplied to PUT and POST requests.
|
|
"""
|
|
class NestedField(serializers.Serializer):
|
|
a = serializers.IntegerField()
|
|
b = serializers.IntegerField()
|
|
|
|
class ExampleSerializer(serializers.Serializer):
|
|
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
|
integer_field = serializers.IntegerField(
|
|
min_value=1, max_value=1000
|
|
)
|
|
char_field = serializers.CharField(
|
|
required=False, min_length=3, max_length=40
|
|
)
|
|
list_field = serializers.ListField(
|
|
child=serializers.ListField(
|
|
child=serializers.IntegerField()
|
|
)
|
|
)
|
|
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': True,
|
|
'read_only': False,
|
|
'label': 'Choice field',
|
|
'choices': [
|
|
{'display_name': 'red', 'value': 'red'},
|
|
{'display_name': 'green', 'value': 'green'},
|
|
{'display_name': 'blue', 'value': 'blue'}
|
|
]
|
|
},
|
|
'integer_field': {
|
|
'type': 'integer',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'Integer field',
|
|
'min_value': 1,
|
|
'max_value': 1000,
|
|
|
|
},
|
|
'char_field': {
|
|
'type': 'string',
|
|
'required': False,
|
|
'read_only': False,
|
|
'label': 'Char field',
|
|
'min_length': 3,
|
|
'max_length': 40
|
|
},
|
|
'list_field': {
|
|
'type': 'list',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'List field',
|
|
'child': {
|
|
'type': 'list',
|
|
'required': True,
|
|
'read_only': False,
|
|
'child': {
|
|
'type': 'integer',
|
|
'required': True,
|
|
'read_only': False
|
|
}
|
|
}
|
|
},
|
|
'nested_field': {
|
|
'type': 'nested object',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'Nested field',
|
|
'children': {
|
|
'a': {
|
|
'type': 'integer',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'A'
|
|
},
|
|
'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_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
|
|
metadata associated with it should not be included in OPTION responses.
|
|
"""
|
|
class ExampleSerializer(serializers.Serializer):
|
|
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
|
integer_field = serializers.IntegerField(max_value=10)
|
|
char_field = serializers.CharField(required=False)
|
|
|
|
class ExampleView(views.APIView):
|
|
"""Example view."""
|
|
def post(self, request):
|
|
pass
|
|
|
|
def put(self, request):
|
|
pass
|
|
|
|
def get_serializer(self):
|
|
return ExampleSerializer()
|
|
|
|
def check_permissions(self, request):
|
|
if request.method == 'POST':
|
|
raise exceptions.PermissionDenied()
|
|
|
|
view = ExampleView.as_view()
|
|
response = view(request=request)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert list(response.data['actions']) == ['PUT']
|
|
|
|
def test_object_permissions(self):
|
|
"""
|
|
If a user does not have object permissions on an action, then any
|
|
metadata associated with it should not be included in OPTION responses.
|
|
"""
|
|
class ExampleSerializer(serializers.Serializer):
|
|
choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
|
|
integer_field = serializers.IntegerField(max_value=10)
|
|
char_field = serializers.CharField(required=False)
|
|
|
|
class ExampleView(views.APIView):
|
|
"""Example view."""
|
|
def post(self, request):
|
|
pass
|
|
|
|
def put(self, request):
|
|
pass
|
|
|
|
def get_serializer(self):
|
|
return ExampleSerializer()
|
|
|
|
def get_object(self):
|
|
if self.request.method == 'PUT':
|
|
raise exceptions.PermissionDenied()
|
|
|
|
view = ExampleView.as_view()
|
|
response = view(request=request)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert list(response.data['actions'].keys()) == ['POST']
|
|
|
|
def test_bug_2455_clone_request(self):
|
|
class ExampleView(views.APIView):
|
|
renderer_classes = (BrowsableAPIRenderer,)
|
|
|
|
def post(self, request):
|
|
pass
|
|
|
|
def get_serializer(self):
|
|
assert hasattr(self.request, 'version')
|
|
return serializers.Serializer()
|
|
|
|
view = ExampleView.as_view()
|
|
view(request=request)
|
|
|
|
def test_bug_2477_clone_request(self):
|
|
class ExampleView(views.APIView):
|
|
renderer_classes = (BrowsableAPIRenderer,)
|
|
|
|
def post(self, request):
|
|
pass
|
|
|
|
def get_serializer(self):
|
|
assert hasattr(self.request, 'versioning_scheme')
|
|
return serializers.Serializer()
|
|
|
|
scheme = versioning.QueryParameterVersioning
|
|
view = ExampleView.as_view(versioning_class=scheme)
|
|
view(request=request)
|
|
|
|
def test_dont_show_hidden_fields(self):
|
|
"""
|
|
HiddenField shouldn't show up in SimpleMetadata at all.
|
|
"""
|
|
class ExampleSerializer(serializers.Serializer):
|
|
integer_field = serializers.IntegerField(max_value=10)
|
|
hidden_field = serializers.HiddenField(default=1)
|
|
|
|
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)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert set(response.data['actions']['POST'].keys()) == {'integer_field'}
|
|
|
|
def test_list_serializer_metadata_returns_info_about_fields_of_child_serializer(self):
|
|
class ExampleSerializer(serializers.Serializer):
|
|
integer_field = serializers.IntegerField(max_value=10)
|
|
char_field = serializers.CharField(required=False)
|
|
|
|
class ExampleListSerializer(serializers.ListSerializer):
|
|
pass
|
|
|
|
options = metadata.SimpleMetadata()
|
|
child_serializer = ExampleSerializer()
|
|
list_serializer = ExampleListSerializer(child=child_serializer)
|
|
assert options.get_serializer_info(list_serializer) == options.get_serializer_info(child_serializer)
|
|
|
|
|
|
class TestSimpleMetadataFieldInfo(TestCase):
|
|
def test_null_boolean_field_info_type(self):
|
|
options = metadata.SimpleMetadata()
|
|
field_info = options.get_field_info(serializers.BooleanField(
|
|
allow_null=True))
|
|
assert field_info['type'] == 'boolean'
|
|
|
|
def test_related_field_choices(self):
|
|
options = metadata.SimpleMetadata()
|
|
BasicModel.objects.create()
|
|
with self.assertNumQueries(0):
|
|
field_info = options.get_field_info(
|
|
serializers.RelatedField(queryset=BasicModel.objects.all())
|
|
)
|
|
assert 'choices' not in field_info
|
|
|
|
def test_decimal_field_info_type(self):
|
|
options = metadata.SimpleMetadata()
|
|
field_info = options.get_field_info(serializers.DecimalField(max_digits=18, decimal_places=4))
|
|
assert field_info['type'] == 'decimal'
|
|
assert field_info['max_digits'] == 18
|
|
assert field_info['decimal_places'] == 4
|
|
|
|
|
|
class TestModelSerializerMetadata(TestCase):
|
|
def test_read_only_primary_key_related_field(self):
|
|
"""
|
|
On generic views OPTIONS should return an 'actions' key with metadata
|
|
on the fields that may be supplied to PUT and POST requests. It should
|
|
not fail when a read_only PrimaryKeyRelatedField is present
|
|
"""
|
|
class Parent(models.Model):
|
|
integer_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(1000)])
|
|
children = models.ManyToManyField('Child')
|
|
name = models.CharField(max_length=100, blank=True, null=True)
|
|
|
|
class Child(models.Model):
|
|
name = models.CharField(max_length=100)
|
|
|
|
class ExampleSerializer(serializers.ModelSerializer):
|
|
children = serializers.PrimaryKeyRelatedField(read_only=True, many=True)
|
|
|
|
class Meta:
|
|
model = Parent
|
|
fields = '__all__'
|
|
|
|
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': {
|
|
'id': {
|
|
'type': 'integer',
|
|
'required': False,
|
|
'read_only': True,
|
|
'label': 'ID'
|
|
},
|
|
'children': {
|
|
'type': 'field',
|
|
'required': False,
|
|
'read_only': True,
|
|
'label': 'Children'
|
|
},
|
|
'integer_field': {
|
|
'type': 'integer',
|
|
'required': True,
|
|
'read_only': False,
|
|
'label': 'Integer field',
|
|
'min_value': 1,
|
|
'max_value': 1000
|
|
},
|
|
'name': {
|
|
'type': 'string',
|
|
'required': False,
|
|
'read_only': False,
|
|
'label': 'Name',
|
|
'max_length': 100
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.data == expected
|