mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-03-03 19:00:17 +03:00
Clean up OPTIONS implementation
This commit is contained in:
parent
760e8642bd
commit
fcaee6e580
|
@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`:
|
|||
|
||||
* Serializer fields now support `label` and `help_text`.
|
||||
* Added `UnicodeJSONRenderer`.
|
||||
* `OPTIONS` requests now return metadata about fields for `POST` and `PUT` requests.
|
||||
* Bugfix: `charset` now properly included in `Content-Type` of responses.
|
||||
* Bugfix: Blank choice now added in browsable API on nullable relationships.
|
||||
* Bugfix: Many to many relationships with `through` tables are now read-only.
|
||||
|
|
|
@ -11,7 +11,6 @@ from decimal import Decimal, DecimalException
|
|||
import inspect
|
||||
import re
|
||||
import warnings
|
||||
|
||||
from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
|
@ -21,7 +20,6 @@ from django.forms import widgets
|
|||
from django.utils.encoding import is_protected_type
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.datastructures import SortedDict
|
||||
|
||||
from rest_framework import ISO_8601
|
||||
from rest_framework.compat import (timezone, parse_date, parse_datetime,
|
||||
parse_time)
|
||||
|
@ -46,6 +44,7 @@ def is_simple_callable(obj):
|
|||
len_defaults = len(defaults) if defaults else 0
|
||||
return len_args <= len_defaults
|
||||
|
||||
|
||||
def get_component(obj, attr_name):
|
||||
"""
|
||||
Given an object, and an attribute name,
|
||||
|
@ -72,18 +71,6 @@ def readable_date_formats(formats):
|
|||
return humanize_strptime(format)
|
||||
|
||||
|
||||
def humanize_form_fields(form):
|
||||
"""Return a humanized description of all the fields in a form.
|
||||
|
||||
:param form: A Django form.
|
||||
:return: A dictionary of {field_label: humanized description}
|
||||
|
||||
"""
|
||||
fields = SortedDict([(name, humanize_field(field))
|
||||
for name, field in form.fields.iteritems()])
|
||||
return fields
|
||||
|
||||
|
||||
def readable_time_formats(formats):
|
||||
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
|
||||
return humanize_strptime(format)
|
||||
|
@ -122,6 +109,7 @@ class Field(object):
|
|||
partial = False
|
||||
use_files = False
|
||||
form_field_class = forms.CharField
|
||||
type_label = 'field'
|
||||
|
||||
def __init__(self, source=None, label=None, help_text=None):
|
||||
self.parent = None
|
||||
|
@ -207,18 +195,17 @@ class Field(object):
|
|||
return {'type': self.type_name}
|
||||
return {}
|
||||
|
||||
@property
|
||||
def humanized(self):
|
||||
humanized = {
|
||||
'type': self.type_name,
|
||||
'required': getattr(self, 'required', False),
|
||||
}
|
||||
optional_attrs = ['read_only', 'help_text', 'label',
|
||||
def metadata(self):
|
||||
metadata = SortedDict()
|
||||
metadata['type'] = self.type_label
|
||||
metadata['required'] = getattr(self, 'required', False)
|
||||
optional_attrs = ['read_only', 'label', 'help_text',
|
||||
'min_length', 'max_length']
|
||||
for attr in optional_attrs:
|
||||
if getattr(self, attr, None) is not None:
|
||||
humanized[attr] = getattr(self, attr)
|
||||
return humanized
|
||||
value = getattr(self, attr, None)
|
||||
if value is not None and value != '':
|
||||
metadata[attr] = force_text(value, strings_only=True)
|
||||
return metadata
|
||||
|
||||
|
||||
class WritableField(Field):
|
||||
|
@ -375,6 +362,7 @@ class ModelField(WritableField):
|
|||
|
||||
class BooleanField(WritableField):
|
||||
type_name = 'BooleanField'
|
||||
type_label = 'boolean'
|
||||
form_field_class = forms.BooleanField
|
||||
widget = widgets.CheckboxInput
|
||||
default_error_messages = {
|
||||
|
@ -397,6 +385,7 @@ class BooleanField(WritableField):
|
|||
|
||||
class CharField(WritableField):
|
||||
type_name = 'CharField'
|
||||
type_label = 'string'
|
||||
form_field_class = forms.CharField
|
||||
|
||||
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
||||
|
@ -415,6 +404,7 @@ class CharField(WritableField):
|
|||
|
||||
class URLField(CharField):
|
||||
type_name = 'URLField'
|
||||
type_label = 'url'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
|
@ -423,6 +413,7 @@ class URLField(CharField):
|
|||
|
||||
class SlugField(CharField):
|
||||
type_name = 'SlugField'
|
||||
type_label = 'slug'
|
||||
form_field_class = forms.SlugField
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -444,6 +435,7 @@ class SlugField(CharField):
|
|||
|
||||
class ChoiceField(WritableField):
|
||||
type_name = 'ChoiceField'
|
||||
type_label = 'multiple choice'
|
||||
form_field_class = forms.ChoiceField
|
||||
widget = widgets.Select
|
||||
default_error_messages = {
|
||||
|
@ -494,6 +486,7 @@ class ChoiceField(WritableField):
|
|||
|
||||
class EmailField(CharField):
|
||||
type_name = 'EmailField'
|
||||
type_label = 'email'
|
||||
form_field_class = forms.EmailField
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -517,6 +510,7 @@ class EmailField(CharField):
|
|||
|
||||
class RegexField(CharField):
|
||||
type_name = 'RegexField'
|
||||
type_label = 'regex'
|
||||
form_field_class = forms.RegexField
|
||||
|
||||
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
|
||||
|
@ -546,6 +540,7 @@ class RegexField(CharField):
|
|||
|
||||
class DateField(WritableField):
|
||||
type_name = 'DateField'
|
||||
type_label = 'date'
|
||||
widget = widgets.DateInput
|
||||
form_field_class = forms.DateField
|
||||
|
||||
|
@ -609,6 +604,7 @@ class DateField(WritableField):
|
|||
|
||||
class DateTimeField(WritableField):
|
||||
type_name = 'DateTimeField'
|
||||
type_label = 'datetime'
|
||||
widget = widgets.DateTimeInput
|
||||
form_field_class = forms.DateTimeField
|
||||
|
||||
|
@ -678,6 +674,7 @@ class DateTimeField(WritableField):
|
|||
|
||||
class TimeField(WritableField):
|
||||
type_name = 'TimeField'
|
||||
type_label = 'time'
|
||||
widget = widgets.TimeInput
|
||||
form_field_class = forms.TimeField
|
||||
|
||||
|
@ -734,6 +731,7 @@ class TimeField(WritableField):
|
|||
|
||||
class IntegerField(WritableField):
|
||||
type_name = 'IntegerField'
|
||||
type_label = 'integer'
|
||||
form_field_class = forms.IntegerField
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -764,6 +762,7 @@ class IntegerField(WritableField):
|
|||
|
||||
class FloatField(WritableField):
|
||||
type_name = 'FloatField'
|
||||
type_label = 'float'
|
||||
form_field_class = forms.FloatField
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -783,6 +782,7 @@ class FloatField(WritableField):
|
|||
|
||||
class DecimalField(WritableField):
|
||||
type_name = 'DecimalField'
|
||||
type_label = 'decimal'
|
||||
form_field_class = forms.DecimalField
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -853,6 +853,7 @@ class DecimalField(WritableField):
|
|||
class FileField(WritableField):
|
||||
use_files = True
|
||||
type_name = 'FileField'
|
||||
type_label = 'file upload'
|
||||
form_field_class = forms.FileField
|
||||
widget = widgets.FileInput
|
||||
|
||||
|
@ -896,6 +897,8 @@ class FileField(WritableField):
|
|||
|
||||
class ImageField(FileField):
|
||||
use_files = True
|
||||
type_name = 'ImageField'
|
||||
type_label = 'image upload'
|
||||
form_field_class = forms.ImageField
|
||||
|
||||
default_error_messages = {
|
||||
|
|
|
@ -3,13 +3,13 @@ Generic views that provide commonly needed behaviour.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
|
||||
from django.core.paginator import Paginator, InvalidPage
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import views, mixins
|
||||
from rest_framework.exceptions import ConfigurationError
|
||||
from rest_framework import views, mixins, exceptions
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.settings import api_settings
|
||||
import warnings
|
||||
|
||||
|
@ -274,7 +274,7 @@ class GenericAPIView(views.APIView):
|
|||
)
|
||||
filter_kwargs = {self.slug_field: slug}
|
||||
else:
|
||||
raise ConfigurationError(
|
||||
raise exceptions.ConfigurationError(
|
||||
'Expected view %s to be called with a URL keyword argument '
|
||||
'named "%s". Fix your URL conf, or set the `.lookup_field` '
|
||||
'attribute on the view correctly.' %
|
||||
|
@ -310,6 +310,41 @@ class GenericAPIView(views.APIView):
|
|||
"""
|
||||
pass
|
||||
|
||||
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':
|
||||
self.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 = self.get_serializer()
|
||||
actions[method] = serializer.metadata()
|
||||
|
||||
if actions:
|
||||
ret['actions'] = actions
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
##########################################################
|
||||
### Concrete view classes that provide method handlers ###
|
||||
|
|
|
@ -521,12 +521,16 @@ class BaseSerializer(WritableField):
|
|||
|
||||
return self.object
|
||||
|
||||
@property
|
||||
def humanized(self):
|
||||
humanized_fields = SortedDict(
|
||||
[(name, field.humanized)
|
||||
for name, field in self.fields.iteritems()])
|
||||
return humanized_fields
|
||||
def metadata(self):
|
||||
"""
|
||||
Return a dictionary of metadata about the fields on the serializer.
|
||||
Useful for things like responding to OPTIONS requests, or generating
|
||||
API schemas for auto-documentation.
|
||||
"""
|
||||
return SortedDict(
|
||||
[(field_name, field.metadata())
|
||||
for field_name, field in six.iteritems(self.fields)]
|
||||
)
|
||||
|
||||
|
||||
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
|
||||
|
|
|
@ -3,17 +3,13 @@ General serializer field tests.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import namedtuple
|
||||
import datetime
|
||||
from decimal import Decimal
|
||||
from uuid import uuid4
|
||||
|
||||
import datetime
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from django.utils.datastructures import SortedDict
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.fields import Field, CharField
|
||||
from rest_framework.serializers import Serializer
|
||||
|
@ -839,7 +835,7 @@ class URLFieldTests(TestCase):
|
|||
'max_length'), 20)
|
||||
|
||||
|
||||
class HumanizedField(TestCase):
|
||||
class FieldMetadata(TestCase):
|
||||
def setUp(self):
|
||||
self.required_field = Field()
|
||||
self.required_field.label = uuid4().hex
|
||||
|
@ -849,41 +845,35 @@ class HumanizedField(TestCase):
|
|||
self.optional_field.label = uuid4().hex
|
||||
self.optional_field.required = False
|
||||
|
||||
def test_type(self):
|
||||
for field in (self.required_field, self.optional_field):
|
||||
self.assertEqual(field.humanized['type'], field.type_name)
|
||||
|
||||
def test_required(self):
|
||||
self.assertEqual(self.required_field.humanized['required'], True)
|
||||
self.assertEqual(self.required_field.metadata()['required'], True)
|
||||
|
||||
def test_optional(self):
|
||||
self.assertEqual(self.optional_field.humanized['required'], False)
|
||||
self.assertEqual(self.optional_field.metadata()['required'], False)
|
||||
|
||||
def test_label(self):
|
||||
for field in (self.required_field, self.optional_field):
|
||||
self.assertEqual(field.humanized['label'], field.label)
|
||||
self.assertEqual(field.metadata()['label'], field.label)
|
||||
|
||||
|
||||
class HumanizableSerializer(Serializer):
|
||||
class MetadataSerializer(Serializer):
|
||||
field1 = CharField(3, required=True)
|
||||
field2 = CharField(10, required=False)
|
||||
|
||||
|
||||
class HumanizedSerializer(TestCase):
|
||||
class MetadataSerializerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.serializer = HumanizableSerializer()
|
||||
self.serializer = MetadataSerializer()
|
||||
|
||||
def test_humanized(self):
|
||||
humanized = self.serializer.humanized
|
||||
def test_serializer_metadata(self):
|
||||
metadata = self.serializer.metadata()
|
||||
expected = {
|
||||
'field1': {u'required': True,
|
||||
u'max_length': 3,
|
||||
u'type': u'CharField',
|
||||
u'read_only': False},
|
||||
'field2': {u'required': False,
|
||||
u'max_length': 10,
|
||||
u'type': u'CharField',
|
||||
u'read_only': False}}
|
||||
self.assertEqual(set(expected.keys()), set(humanized.keys()))
|
||||
for k, v in humanized.iteritems():
|
||||
self.assertEqual(v, expected[k])
|
||||
'field1': {'required': True,
|
||||
'max_length': 3,
|
||||
'type': 'string',
|
||||
'read_only': False},
|
||||
'field2': {'required': False,
|
||||
'max_length': 10,
|
||||
'type': 'string',
|
||||
'read_only': False}}
|
||||
self.assertEqual(expected, metadata)
|
||||
|
|
|
@ -122,21 +122,24 @@ class TestRootView(TestCase):
|
|||
],
|
||||
'name': 'Root',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {}
|
||||
}
|
||||
expected['actions']['GET'] = {}
|
||||
expected['actions']['POST'] = {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'String',
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'Integer',
|
||||
},
|
||||
'actions': {
|
||||
'POST': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
"label": "Text comes here",
|
||||
"help_text": "Text description."
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
@ -239,9 +242,9 @@ class TestInstanceView(TestCase):
|
|||
"""
|
||||
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
|
||||
"""
|
||||
request = factory.options('/')
|
||||
with self.assertNumQueries(0):
|
||||
response = self.view(request).render()
|
||||
request = factory.options('/1')
|
||||
with self.assertNumQueries(1):
|
||||
response = self.view(request, pk=1).render()
|
||||
expected = {
|
||||
'parses': [
|
||||
'application/json',
|
||||
|
@ -254,24 +257,25 @@ class TestInstanceView(TestCase):
|
|||
],
|
||||
'name': 'Instance',
|
||||
'description': 'Example description for OPTIONS.',
|
||||
'actions': {}
|
||||
}
|
||||
for method in ('GET', 'DELETE'):
|
||||
expected['actions'][method] = {}
|
||||
for method in ('PATCH', 'PUT'):
|
||||
expected['actions'][method] = {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'String',
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'Integer',
|
||||
},
|
||||
'actions': {
|
||||
'PUT': {
|
||||
'text': {
|
||||
'max_length': 100,
|
||||
'read_only': False,
|
||||
'required': True,
|
||||
'type': 'string',
|
||||
'label': 'Text comes here',
|
||||
'help_text': 'Text description.'
|
||||
},
|
||||
'id': {
|
||||
'read_only': True,
|
||||
'required': False,
|
||||
'type': 'integer',
|
||||
'label': 'ID',
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
|
|
|
@ -114,44 +114,41 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['POST', 'GET',])
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'DELETE', 'GET',])
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
|
||||
def test_options_disallowed(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['GET',])
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['GET',])
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
def test_options_updateonly(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['GET',])
|
||||
self.assertNotIn('actions', response.data)
|
||||
|
||||
request = factory.options('/1', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.updateonly_credentials)
|
||||
response = instance_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'GET',])
|
||||
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
|
||||
|
||||
|
||||
class OwnerModel(models.Model):
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.core.cache import cache
|
|||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import unittest
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.compat import yaml, etree, patterns, url, include
|
||||
from rest_framework.response import Response
|
||||
|
@ -238,6 +239,13 @@ class JSONRendererTests(TestCase):
|
|||
Tests specific to the JSON Renderer
|
||||
"""
|
||||
|
||||
def test_render_lazy_strings(self):
|
||||
"""
|
||||
JSONRenderer should deal with lazy translated strings.
|
||||
"""
|
||||
ret = JSONRenderer().render(_('test'))
|
||||
self.assertEqual(ret, b'"test"')
|
||||
|
||||
def test_without_content_type_args(self):
|
||||
"""
|
||||
Test basic JSON rendering.
|
||||
|
|
|
@ -3,7 +3,8 @@ Helper classes for parsers.
|
|||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.utils.datastructures import SortedDict
|
||||
from rest_framework.compat import timezone
|
||||
from django.utils.functional import Promise
|
||||
from rest_framework.compat import timezone, force_text
|
||||
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
|
||||
import datetime
|
||||
import decimal
|
||||
|
@ -19,7 +20,9 @@ class JSONEncoder(json.JSONEncoder):
|
|||
def default(self, o):
|
||||
# For Date Time string spec, see ECMA 262
|
||||
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
|
||||
if isinstance(o, datetime.datetime):
|
||||
if isinstance(o, Promise):
|
||||
return force_text(o)
|
||||
elif isinstance(o, datetime.datetime):
|
||||
r = o.isoformat()
|
||||
if o.microsecond:
|
||||
r = r[:23] + r[26:]
|
||||
|
|
|
@ -5,12 +5,11 @@ from __future__ import unicode_literals
|
|||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponse
|
||||
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 View
|
||||
from rest_framework.fields import humanize_form_fields
|
||||
from rest_framework.request import clone_request, Request
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.formatting import get_view_name, get_view_description
|
||||
|
@ -54,53 +53,6 @@ class APIView(View):
|
|||
'Vary': 'Accept'
|
||||
}
|
||||
|
||||
def metadata(self, request):
|
||||
content = {
|
||||
'name': get_view_name(self.__class__),
|
||||
'description': get_view_description(self.__class__),
|
||||
'renders': [renderer.media_type for renderer in self.renderer_classes],
|
||||
'parses': [parser.media_type for parser in self.parser_classes],
|
||||
}
|
||||
content['actions'] = self.action_metadata(request)
|
||||
|
||||
return content
|
||||
|
||||
def action_metadata(self, request):
|
||||
"""Return a dictionary with the fields required fo reach allowed method. If no method is allowed,
|
||||
return an empty dictionary.
|
||||
|
||||
:param request: Request for which to return the metadata of the allowed methods.
|
||||
:return: A dictionary of the form {method: {field: {field attribute: value}}}
|
||||
"""
|
||||
actions = {}
|
||||
for method in self.allowed_methods:
|
||||
# skip HEAD and OPTIONS
|
||||
if method in ('HEAD', 'OPTIONS'):
|
||||
continue
|
||||
|
||||
cloned_request = clone_request(request, method)
|
||||
try:
|
||||
self.check_permissions(cloned_request)
|
||||
|
||||
# TODO: discuss whether and how to expose parameters like e.g. filter or paginate
|
||||
if method in ('GET', 'DELETE'):
|
||||
actions[method] = {}
|
||||
continue
|
||||
|
||||
if not hasattr(self, 'get_serializer'):
|
||||
continue
|
||||
serializer = self.get_serializer()
|
||||
if serializer is not None:
|
||||
actions[method] = serializer.humanized
|
||||
except exceptions.PermissionDenied:
|
||||
# don't add this method
|
||||
pass
|
||||
except exceptions.NotAuthenticated:
|
||||
# don't add this method
|
||||
pass
|
||||
|
||||
return actions if len(actions) > 0 else None
|
||||
|
||||
def http_method_not_allowed(self, request, *args, **kwargs):
|
||||
"""
|
||||
If `request.method` does not correspond to a handler method,
|
||||
|
@ -383,3 +335,15 @@ class APIView(View):
|
|||
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.
|
||||
"""
|
||||
ret = SortedDict()
|
||||
ret['name'] = get_view_name(self.__class__)
|
||||
ret['description'] = get_view_description(self.__class__)
|
||||
ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
|
||||
ret['parses'] = [parser.media_type for parser in self.parser_classes]
|
||||
return ret
|
||||
|
|
Loading…
Reference in New Issue
Block a user