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 __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
from django.core.exceptions import PermissionDenied
|
|
||||||
from django.core.paginator import Paginator, InvalidPage
|
from django.core.paginator import Paginator, InvalidPage
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
from django.shortcuts import get_object_or_404 as _get_object_or_404
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from rest_framework import views, mixins, exceptions
|
from rest_framework import views, mixins
|
||||||
from rest_framework.request import clone_request
|
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
|
|
||||||
|
|
||||||
|
@ -249,53 +247,6 @@ class GenericAPIView(views.APIView):
|
||||||
|
|
||||||
return obj
|
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
|
# Concrete view classes that provide method handlers
|
||||||
# by composing the mixin classes with the base view.
|
# 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 (
|
from rest_framework.utils.field_mapping import (
|
||||||
get_url_kwargs, get_field_kwargs,
|
get_url_kwargs, get_field_kwargs,
|
||||||
get_relation_kwargs, get_nested_relation_kwargs,
|
get_relation_kwargs, get_nested_relation_kwargs,
|
||||||
lookup_class
|
ClassLookupDict
|
||||||
)
|
)
|
||||||
import copy
|
import copy
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ModelSerializer(Serializer):
|
class ModelSerializer(Serializer):
|
||||||
_field_mapping = {
|
_field_mapping = ClassLookupDict({
|
||||||
models.AutoField: IntegerField,
|
models.AutoField: IntegerField,
|
||||||
models.BigIntegerField: IntegerField,
|
models.BigIntegerField: IntegerField,
|
||||||
models.BooleanField: BooleanField,
|
models.BooleanField: BooleanField,
|
||||||
|
@ -341,7 +341,7 @@ class ModelSerializer(Serializer):
|
||||||
models.TextField: CharField,
|
models.TextField: CharField,
|
||||||
models.TimeField: TimeField,
|
models.TimeField: TimeField,
|
||||||
models.URLField: URLField,
|
models.URLField: URLField,
|
||||||
}
|
})
|
||||||
_related_class = PrimaryKeyRelatedField
|
_related_class = PrimaryKeyRelatedField
|
||||||
|
|
||||||
def create(self, attrs):
|
def create(self, attrs):
|
||||||
|
@ -400,7 +400,7 @@ class ModelSerializer(Serializer):
|
||||||
elif field_name in info.fields_and_pk:
|
elif field_name in info.fields_and_pk:
|
||||||
# Create regular model fields.
|
# Create regular model fields.
|
||||||
model_field = info.fields_and_pk[field_name]
|
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)
|
kwargs = get_field_kwargs(field_name, model_field)
|
||||||
if 'choices' in kwargs:
|
if 'choices' in kwargs:
|
||||||
# Fields with choices get coerced into `ChoiceField`
|
# Fields with choices get coerced into `ChoiceField`
|
||||||
|
|
|
@ -45,6 +45,7 @@ DEFAULTS = {
|
||||||
),
|
),
|
||||||
'DEFAULT_THROTTLE_CLASSES': (),
|
'DEFAULT_THROTTLE_CLASSES': (),
|
||||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
|
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
|
||||||
|
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||||
|
|
||||||
# Genric view behavior
|
# Genric view behavior
|
||||||
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
|
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
|
||||||
|
@ -121,6 +122,7 @@ IMPORT_STRINGS = (
|
||||||
'DEFAULT_PERMISSION_CLASSES',
|
'DEFAULT_PERMISSION_CLASSES',
|
||||||
'DEFAULT_THROTTLE_CLASSES',
|
'DEFAULT_THROTTLE_CLASSES',
|
||||||
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
|
||||||
|
'DEFAULT_METADATA_CLASS',
|
||||||
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
'DEFAULT_MODEL_SERIALIZER_CLASS',
|
||||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
|
||||||
'DEFAULT_FILTER_BACKENDS',
|
'DEFAULT_FILTER_BACKENDS',
|
||||||
|
|
|
@ -9,17 +9,21 @@ from rest_framework.compat import clean_manytomany_helptext
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
|
||||||
def lookup_class(mapping, instance):
|
class ClassLookupDict(object):
|
||||||
"""
|
"""
|
||||||
Takes a dictionary with classes as keys, and an object.
|
Takes a dictionary with classes as keys.
|
||||||
Traverses the object's inheritance hierarchy in method
|
Lookups against this object will traverses the object's inheritance
|
||||||
resolution order, and returns the first matching value
|
hierarchy in method resolution order, and returns the first matching value
|
||||||
from the dictionary or raises a KeyError if nothing matches.
|
from the dictionary or raises a KeyError if nothing matches.
|
||||||
"""
|
"""
|
||||||
for cls in inspect.getmro(instance.__class__):
|
def __init__(self, mapping):
|
||||||
if cls in mapping:
|
self.mapping = mapping
|
||||||
return mapping[cls]
|
|
||||||
raise KeyError('Class %s not found in lookup.', cls.__name__)
|
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):
|
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.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.datastructures import SortedDict
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from rest_framework import status, exceptions
|
from rest_framework import status, exceptions
|
||||||
from rest_framework.compat import smart_text, HttpResponseBase, View
|
from rest_framework.compat import smart_text, HttpResponseBase, View
|
||||||
|
@ -99,6 +98,7 @@ class APIView(View):
|
||||||
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
|
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
|
||||||
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
|
||||||
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
|
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.
|
# Allow dependancy injection of other settings to make testing easier.
|
||||||
settings = api_settings
|
settings = api_settings
|
||||||
|
@ -418,22 +418,8 @@ class APIView(View):
|
||||||
def options(self, request, *args, **kwargs):
|
def options(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Handler method for HTTP 'OPTIONS' request.
|
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)
|
if self.metadata_class is None:
|
||||||
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||||
def metadata(self, request):
|
data = self.metadata_class().determine_metadata(request, self)
|
||||||
"""
|
return Response(data, status=status.HTTP_200_OK)
|
||||||
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
|
|
||||||
|
|
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