mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-10 19:56:59 +03:00
OPTIONS support
This commit is contained in:
parent
aa84432f9b
commit
f4b1dcb167
|
@ -4,13 +4,11 @@ Generic views that provide commonly needed behaviour.
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator, InvalidPage
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import views, mixins, exceptions
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework import views, mixins
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
|
@ -249,53 +247,6 @@ class GenericAPIView(views.APIView):
|
|||
|
||||
return obj
|
||||
|
||||
# The following are placeholder methods,
|
||||
# and are intended to be overridden.
|
||||
#
|
||||
# The are not called by GenericAPIView directly,
|
||||
# but are used by the mixin methods.
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
|
||||
We override the default behavior, and add some extra information
|
||||
about the required request body for POST and PUT operations.
|
||||
"""
|
||||
ret = super(GenericAPIView, self).metadata(request)
|
||||
|
||||
actions = {}
|
||||
for method in ('PUT', 'POST'):
|
||||
if method not in self.allowed_methods:
|
||||
continue
|
||||
|
||||
cloned_request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
self.check_permissions(cloned_request)
|
||||
# Test object permissions
|
||||
if method == 'PUT':
|
||||
try:
|
||||
self.get_object()
|
||||
except Http404:
|
||||
# Http404 should be acceptable and the serializer
|
||||
# metadata should be populated. Except this so the
|
||||
# outer "else" clause of the try-except-else block
|
||||
# will be executed.
|
||||
pass
|
||||
except (exceptions.APIException, PermissionDenied):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = self.get_serializer()
|
||||
actions[method] = serializer.metadata()
|
||||
|
||||
if actions:
|
||||
ret['actions'] = actions
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# Concrete view classes that provide method handlers
|
||||
# by composing the mixin classes with the base view.
|
||||
|
|
126
rest_framework/metadata.py
Normal file
126
rest_framework/metadata.py
Normal file
|
@ -0,0 +1,126 @@
|
|||
"""
|
||||
The metadata API is used to allow cusomization of how `OPTIONS` requests
|
||||
are handled. We currently provide a single default implementation that returns
|
||||
some fairly ad-hoc information about the view.
|
||||
|
||||
Future implementations might use JSON schema or other definations in order
|
||||
to return this information in a more standardized way.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.utils import six
|
||||
from django.utils.datastructures import SortedDict
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import force_text
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
|
||||
class BaseMetadata(object):
|
||||
def determine_metadata(self, request, view):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
"""
|
||||
raise NotImplementedError(".determine_metadata() must be overridden.")
|
||||
|
||||
|
||||
class SimpleMetadata(BaseMetadata):
|
||||
"""
|
||||
This is the default metadata implementation.
|
||||
It returns an ad-hoc set of information about the view.
|
||||
There are not any formalized standards for `OPTIONS` responses
|
||||
for us to base this on.
|
||||
"""
|
||||
label_lookup = ClassLookupDict({
|
||||
serializers.Field: 'field',
|
||||
serializers.BooleanField: 'boolean',
|
||||
serializers.CharField: 'string',
|
||||
serializers.URLField: 'url',
|
||||
serializers.EmailField: 'email',
|
||||
serializers.RegexField: 'regex',
|
||||
serializers.SlugField: 'slug',
|
||||
serializers.IntegerField: 'integer',
|
||||
serializers.FloatField: 'float',
|
||||
serializers.DecimalField: 'decimal',
|
||||
serializers.DateField: 'date',
|
||||
serializers.DateTimeField: 'datetime',
|
||||
serializers.TimeField: 'time',
|
||||
serializers.ChoiceField: 'choice',
|
||||
serializers.MultipleChoiceField: 'multiple choice',
|
||||
serializers.FileField: 'file upload',
|
||||
serializers.ImageField: 'image upload',
|
||||
})
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
metadata = SortedDict()
|
||||
metadata['name'] = view.get_view_name()
|
||||
metadata['description'] = view.get_view_description()
|
||||
metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
|
||||
metadata['parses'] = [parser.media_type for parser in view.parser_classes]
|
||||
if hasattr(view, 'get_serializer'):
|
||||
actions = self.determine_actions(request, view)
|
||||
if actions:
|
||||
metadata['actions'] = actions
|
||||
return metadata
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
"""
|
||||
For generic class based views we return information about
|
||||
the fields that are accepted for 'PUT' and 'POST' methods.
|
||||
"""
|
||||
actions = {}
|
||||
for method in set(['PUT', 'POST']) & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
if hasattr(view, 'check_permissions'):
|
||||
view.check_permissions(view.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT' and hasattr(view, 'get_object'):
|
||||
view.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = view.get_serializer()
|
||||
actions[method] = self.get_serializer_info(serializer)
|
||||
finally:
|
||||
view.request = request
|
||||
|
||||
return actions
|
||||
|
||||
def get_serializer_info(self, serializer):
|
||||
"""
|
||||
Given an instance of a serializer, return a dictionary of metadata
|
||||
about its fields.
|
||||
"""
|
||||
return SortedDict([
|
||||
(field_name, self.get_field_info(field))
|
||||
for field_name, field in six.iteritems(serializer.fields)
|
||||
])
|
||||
|
||||
def get_field_info(self, field):
|
||||
"""
|
||||
Given an instance of a serializer field, return a dictionary
|
||||
of metadata about it.
|
||||
"""
|
||||
field_info = SortedDict()
|
||||
field_info['type'] = self.label_lookup[field]
|
||||
field_info['required'] = getattr(field, 'required', False)
|
||||
|
||||
for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']:
|
||||
value = getattr(field, attr, None)
|
||||
if value is not None and value != '':
|
||||
field_info[attr] = force_text(value, strings_only=True)
|
||||
|
||||
if hasattr(field, 'choices'):
|
||||
field_info['choices'] = [
|
||||
{'value': choice_value, 'display_name': choice_name}
|
||||
for choice_value, choice_name in field.choices.items()
|
||||
]
|
||||
|
||||
return field_info
|
|
@ -21,7 +21,7 @@ from rest_framework.utils import html, model_meta, representation
|
|||
from rest_framework.utils.field_mapping import (
|
||||
get_url_kwargs, get_field_kwargs,
|
||||
get_relation_kwargs, get_nested_relation_kwargs,
|
||||
lookup_class
|
||||
ClassLookupDict
|
||||
)
|
||||
import copy
|
||||
import inspect
|
||||
|
@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer):
|
|||
|
||||
|
||||
class ModelSerializer(Serializer):
|
||||
_field_mapping = {
|
||||
_field_mapping = ClassLookupDict({
|
||||
models.AutoField: IntegerField,
|
||||
models.BigIntegerField: IntegerField,
|
||||
models.BooleanField: BooleanField,
|
||||
|
@ -341,7 +341,7 @@ class ModelSerializer(Serializer):
|
|||
models.TextField: CharField,
|
||||
models.TimeField: TimeField,
|
||||
models.URLField: URLField,
|
||||
}
|
||||
})
|
||||
_related_class = PrimaryKeyRelatedField
|
||||
|
||||
def create(self, attrs):
|
||||
|
@ -400,7 +400,7 @@ class ModelSerializer(Serializer):
|
|||
elif field_name in info.fields_and_pk:
|
||||
# Create regular model fields.
|
||||
model_field = info.fields_and_pk[field_name]
|
||||
field_cls = lookup_class(self._field_mapping, model_field)
|
||||
field_cls = self._field_mapping[model_field]
|
||||
kwargs = get_field_kwargs(field_name, model_field)
|
||||
if 'choices' in kwargs:
|
||||
# Fields with choices get coerced into `ChoiceField`
|
||||
|
|
|
@ -45,6 +45,7 @@ DEFAULTS = {
|
|||
),
|
||||
'DEFAULT_THROTTLE_CLASSES': (),
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
|
||||
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||
|
||||
# Genric view behavior
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
|
||||
|
@ -121,6 +122,7 @@ IMPORT_STRINGS = (
|
|||
'DEFAULT_PERMISSION_CLASSES',
|
||||
'DEFAULT_THROTTLE_CLASSES',
|
||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||
'DEFAULT_METADATA_CLASS',
|
||||
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||
'DEFAULT_FILTER_BACKENDS',
|
||||
|
|
|
@ -9,17 +9,21 @@ from rest_framework.compat import clean_manytomany_helptext
|
|||
import inspect
|
||||
|
||||
|
||||
def lookup_class(mapping, instance):
|
||||
class ClassLookupDict(object):
|
||||
"""
|
||||
Takes a dictionary with classes as keys, and an object.
|
||||
Traverses the object's inheritance hierarchy in method
|
||||
resolution order, and returns the first matching value
|
||||
Takes a dictionary with classes as keys.
|
||||
Lookups against this object will traverses the object's inheritance
|
||||
hierarchy in method resolution order, and returns the first matching value
|
||||
from the dictionary or raises a KeyError if nothing matches.
|
||||
"""
|
||||
for cls in inspect.getmro(instance.__class__):
|
||||
if cls in mapping:
|
||||
return mapping[cls]
|
||||
raise KeyError('Class %s not found in lookup.', cls.__name__)
|
||||
def __init__(self, mapping):
|
||||
self.mapping = mapping
|
||||
|
||||
def __getitem__(self, key):
|
||||
for cls in inspect.getmro(key.__class__):
|
||||
if cls in self.mapping:
|
||||
return self.mapping[cls]
|
||||
raise KeyError('Class %s not found in lookup.', cls.__name__)
|
||||
|
||||
|
||||
def needs_label(model_field, field_name):
|
||||
|
|
|
@ -5,7 +5,6 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
|
||||
from django.http import Http404
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework import status, exceptions
|
||||
from rest_framework.compat import smart_text, HttpResponseBase, View
|
||||
|
@ -99,6 +98,7 @@ class APIView(View):
|
|||
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
|
||||
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
|
||||
metadata_class = api_settings.DEFAULT_METADATA_CLASS
|
||||
|
||||
# Allow dependancy injection of other settings to make testing easier.
|
||||
settings = api_settings
|
||||
|
@ -418,22 +418,8 @@ class APIView(View):
|
|||
def options(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handler method for HTTP 'OPTIONS' request.
|
||||
We may as well implement this as Django will otherwise provide
|
||||
a less useful default implementation.
|
||||
"""
|
||||
return Response(self.metadata(request), status=status.HTTP_200_OK)
|
||||
|
||||
def metadata(self, request):
|
||||
"""
|
||||
Return a dictionary of metadata about the view.
|
||||
Used to return responses for OPTIONS requests.
|
||||
"""
|
||||
# By default we can't provide any form-like information, however the
|
||||
# generic views override this implementation and add additional
|
||||
# information for POST and PUT methods, based on the serializer.
|
||||
ret = SortedDict()
|
||||
ret['name'] = self.get_view_name()
|
||||
ret['description'] = self.get_view_description()
|
||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||
return ret
|
||||
if self.metadata_class is None:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
data = self.metadata_class().determine_metadata(request, self)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
|
166
tests/test_metadata.py
Normal file
166
tests/test_metadata.py
Normal file
|
@ -0,0 +1,166 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import exceptions, serializers, views
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.test import APIRequestFactory
|
||||
import pytest
|
||||
|
||||
request = Request(APIRequestFactory().options('/'))
|
||||
|
||||
|
||||
class TestMetadata:
|
||||
def test_metadata(self):
|
||||
"""
|
||||
OPTIONS requests to views should return a valid 200 response.
|
||||
"""
|
||||
class ExampleView(views.APIView):
|
||||
"""Example view."""
|
||||
pass
|
||||
|
||||
response = ExampleView().options(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 == 200
|
||||
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
|
||||
|
||||
with pytest.raises(exceptions.MethodNotAllowed):
|
||||
ExampleView().options(request=request)
|
||||
|
||||
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 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 get_serializer(self):
|
||||
return ExampleSerializer()
|
||||
|
||||
response = ExampleView().options(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': 'blue', 'value': 'blue'},
|
||||
{'display_name': 'green', 'value': 'green'},
|
||||
{'display_name': 'red', 'value': 'red'}
|
||||
]
|
||||
},
|
||||
'integer_field': {
|
||||
'type': 'integer',
|
||||
'required': True,
|
||||
'read_only': False,
|
||||
'label': 'Integer field'
|
||||
},
|
||||
'char_field': {
|
||||
'type': 'string',
|
||||
'required': False,
|
||||
'read_only': False,
|
||||
'label': 'Char field'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert response.status_code == 200
|
||||
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()
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
assert response.status_code == 200
|
||||
assert list(response.data['actions'].keys()) == ['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()
|
||||
|
||||
response = ExampleView().options(request=request)
|
||||
assert response.status_code == 200
|
||||
assert list(response.data['actions'].keys()) == ['POST']
|
Loading…
Reference in New Issue
Block a user