Clean up OPTIONS implementation

This commit is contained in:
Tom Christie 2013-05-24 23:44:23 +01:00
parent 760e8642bd
commit fcaee6e580
10 changed files with 175 additions and 166 deletions

View File

@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`:
* Serializer fields now support `label` and `help_text`. * Serializer fields now support `label` and `help_text`.
* Added `UnicodeJSONRenderer`. * 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: `charset` now properly included in `Content-Type` of responses.
* Bugfix: Blank choice now added in browsable API on nullable relationships. * Bugfix: Blank choice now added in browsable API on nullable relationships.
* Bugfix: Many to many relationships with `through` tables are now read-only. * Bugfix: Many to many relationships with `through` tables are now read-only.

View File

@ -11,7 +11,6 @@ from decimal import Decimal, DecimalException
import inspect import inspect
import re import re
import warnings import warnings
from django.core import validators from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.conf import settings 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.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601 from rest_framework import ISO_8601
from rest_framework.compat import (timezone, parse_date, parse_datetime, from rest_framework.compat import (timezone, parse_date, parse_datetime,
parse_time) parse_time)
@ -46,6 +44,7 @@ def is_simple_callable(obj):
len_defaults = len(defaults) if defaults else 0 len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults return len_args <= len_defaults
def get_component(obj, attr_name): def get_component(obj, attr_name):
""" """
Given an object, and an attribute name, Given an object, and an attribute name,
@ -72,18 +71,6 @@ def readable_date_formats(formats):
return humanize_strptime(format) 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): def readable_time_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]') format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
return humanize_strptime(format) return humanize_strptime(format)
@ -122,6 +109,7 @@ class Field(object):
partial = False partial = False
use_files = False use_files = False
form_field_class = forms.CharField form_field_class = forms.CharField
type_label = 'field'
def __init__(self, source=None, label=None, help_text=None): def __init__(self, source=None, label=None, help_text=None):
self.parent = None self.parent = None
@ -207,18 +195,17 @@ class Field(object):
return {'type': self.type_name} return {'type': self.type_name}
return {} return {}
@property def metadata(self):
def humanized(self): metadata = SortedDict()
humanized = { metadata['type'] = self.type_label
'type': self.type_name, metadata['required'] = getattr(self, 'required', False)
'required': getattr(self, 'required', False), optional_attrs = ['read_only', 'label', 'help_text',
}
optional_attrs = ['read_only', 'help_text', 'label',
'min_length', 'max_length'] 'min_length', 'max_length']
for attr in optional_attrs: for attr in optional_attrs:
if getattr(self, attr, None) is not None: value = getattr(self, attr, None)
humanized[attr] = getattr(self, attr) if value is not None and value != '':
return humanized metadata[attr] = force_text(value, strings_only=True)
return metadata
class WritableField(Field): class WritableField(Field):
@ -375,6 +362,7 @@ class ModelField(WritableField):
class BooleanField(WritableField): class BooleanField(WritableField):
type_name = 'BooleanField' type_name = 'BooleanField'
type_label = 'boolean'
form_field_class = forms.BooleanField form_field_class = forms.BooleanField
widget = widgets.CheckboxInput widget = widgets.CheckboxInput
default_error_messages = { default_error_messages = {
@ -397,6 +385,7 @@ class BooleanField(WritableField):
class CharField(WritableField): class CharField(WritableField):
type_name = 'CharField' type_name = 'CharField'
type_label = 'string'
form_field_class = forms.CharField form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs): def __init__(self, max_length=None, min_length=None, *args, **kwargs):
@ -415,6 +404,7 @@ class CharField(WritableField):
class URLField(CharField): class URLField(CharField):
type_name = 'URLField' type_name = 'URLField'
type_label = 'url'
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['validators'] = [validators.URLValidator()] kwargs['validators'] = [validators.URLValidator()]
@ -423,6 +413,7 @@ class URLField(CharField):
class SlugField(CharField): class SlugField(CharField):
type_name = 'SlugField' type_name = 'SlugField'
type_label = 'slug'
form_field_class = forms.SlugField form_field_class = forms.SlugField
default_error_messages = { default_error_messages = {
@ -444,6 +435,7 @@ class SlugField(CharField):
class ChoiceField(WritableField): class ChoiceField(WritableField):
type_name = 'ChoiceField' type_name = 'ChoiceField'
type_label = 'multiple choice'
form_field_class = forms.ChoiceField form_field_class = forms.ChoiceField
widget = widgets.Select widget = widgets.Select
default_error_messages = { default_error_messages = {
@ -494,6 +486,7 @@ class ChoiceField(WritableField):
class EmailField(CharField): class EmailField(CharField):
type_name = 'EmailField' type_name = 'EmailField'
type_label = 'email'
form_field_class = forms.EmailField form_field_class = forms.EmailField
default_error_messages = { default_error_messages = {
@ -517,6 +510,7 @@ class EmailField(CharField):
class RegexField(CharField): class RegexField(CharField):
type_name = 'RegexField' type_name = 'RegexField'
type_label = 'regex'
form_field_class = forms.RegexField form_field_class = forms.RegexField
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs): def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
@ -546,6 +540,7 @@ class RegexField(CharField):
class DateField(WritableField): class DateField(WritableField):
type_name = 'DateField' type_name = 'DateField'
type_label = 'date'
widget = widgets.DateInput widget = widgets.DateInput
form_field_class = forms.DateField form_field_class = forms.DateField
@ -609,6 +604,7 @@ class DateField(WritableField):
class DateTimeField(WritableField): class DateTimeField(WritableField):
type_name = 'DateTimeField' type_name = 'DateTimeField'
type_label = 'datetime'
widget = widgets.DateTimeInput widget = widgets.DateTimeInput
form_field_class = forms.DateTimeField form_field_class = forms.DateTimeField
@ -678,6 +674,7 @@ class DateTimeField(WritableField):
class TimeField(WritableField): class TimeField(WritableField):
type_name = 'TimeField' type_name = 'TimeField'
type_label = 'time'
widget = widgets.TimeInput widget = widgets.TimeInput
form_field_class = forms.TimeField form_field_class = forms.TimeField
@ -734,6 +731,7 @@ class TimeField(WritableField):
class IntegerField(WritableField): class IntegerField(WritableField):
type_name = 'IntegerField' type_name = 'IntegerField'
type_label = 'integer'
form_field_class = forms.IntegerField form_field_class = forms.IntegerField
default_error_messages = { default_error_messages = {
@ -764,6 +762,7 @@ class IntegerField(WritableField):
class FloatField(WritableField): class FloatField(WritableField):
type_name = 'FloatField' type_name = 'FloatField'
type_label = 'float'
form_field_class = forms.FloatField form_field_class = forms.FloatField
default_error_messages = { default_error_messages = {
@ -783,6 +782,7 @@ class FloatField(WritableField):
class DecimalField(WritableField): class DecimalField(WritableField):
type_name = 'DecimalField' type_name = 'DecimalField'
type_label = 'decimal'
form_field_class = forms.DecimalField form_field_class = forms.DecimalField
default_error_messages = { default_error_messages = {
@ -853,6 +853,7 @@ class DecimalField(WritableField):
class FileField(WritableField): class FileField(WritableField):
use_files = True use_files = True
type_name = 'FileField' type_name = 'FileField'
type_label = 'file upload'
form_field_class = forms.FileField form_field_class = forms.FileField
widget = widgets.FileInput widget = widgets.FileInput
@ -896,6 +897,8 @@ class FileField(WritableField):
class ImageField(FileField): class ImageField(FileField):
use_files = True use_files = True
type_name = 'ImageField'
type_label = 'image upload'
form_field_class = forms.ImageField form_field_class = forms.ImageField
default_error_messages = { default_error_messages = {

View File

@ -3,13 +3,13 @@ Generic views that provide commonly needed behaviour.
""" """
from __future__ import unicode_literals 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.core.paginator import Paginator, InvalidPage
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from rest_framework import views, mixins from rest_framework import views, mixins, exceptions
from rest_framework.exceptions import ConfigurationError from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings import warnings
@ -274,7 +274,7 @@ class GenericAPIView(views.APIView):
) )
filter_kwargs = {self.slug_field: slug} filter_kwargs = {self.slug_field: slug}
else: else:
raise ConfigurationError( raise exceptions.ConfigurationError(
'Expected view %s to be called with a URL keyword argument ' 'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` ' 'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' % 'attribute on the view correctly.' %
@ -310,6 +310,41 @@ class GenericAPIView(views.APIView):
""" """
pass 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 ### ### Concrete view classes that provide method handlers ###

View File

@ -521,12 +521,16 @@ class BaseSerializer(WritableField):
return self.object return self.object
@property def metadata(self):
def humanized(self): """
humanized_fields = SortedDict( Return a dictionary of metadata about the fields on the serializer.
[(name, field.humanized) Useful for things like responding to OPTIONS requests, or generating
for name, field in self.fields.iteritems()]) API schemas for auto-documentation.
return humanized_fields """
return SortedDict(
[(field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)]
)
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):

View File

@ -3,17 +3,13 @@ General serializer field tests.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import namedtuple import datetime
from decimal import Decimal from decimal import Decimal
from uuid import uuid4 from uuid import uuid4
import datetime
from django import forms
from django.core import validators from django.core import validators
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import Field, CharField from rest_framework.fields import Field, CharField
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
@ -839,7 +835,7 @@ class URLFieldTests(TestCase):
'max_length'), 20) 'max_length'), 20)
class HumanizedField(TestCase): class FieldMetadata(TestCase):
def setUp(self): def setUp(self):
self.required_field = Field() self.required_field = Field()
self.required_field.label = uuid4().hex self.required_field.label = uuid4().hex
@ -849,41 +845,35 @@ class HumanizedField(TestCase):
self.optional_field.label = uuid4().hex self.optional_field.label = uuid4().hex
self.optional_field.required = False 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): def test_required(self):
self.assertEqual(self.required_field.humanized['required'], True) self.assertEqual(self.required_field.metadata()['required'], True)
def test_optional(self): def test_optional(self):
self.assertEqual(self.optional_field.humanized['required'], False) self.assertEqual(self.optional_field.metadata()['required'], False)
def test_label(self): def test_label(self):
for field in (self.required_field, self.optional_field): 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) field1 = CharField(3, required=True)
field2 = CharField(10, required=False) field2 = CharField(10, required=False)
class HumanizedSerializer(TestCase): class MetadataSerializerTestCase(TestCase):
def setUp(self): def setUp(self):
self.serializer = HumanizableSerializer() self.serializer = MetadataSerializer()
def test_humanized(self): def test_serializer_metadata(self):
humanized = self.serializer.humanized metadata = self.serializer.metadata()
expected = { expected = {
'field1': {u'required': True, 'field1': {'required': True,
u'max_length': 3, 'max_length': 3,
u'type': u'CharField', 'type': 'string',
u'read_only': False}, 'read_only': False},
'field2': {u'required': False, 'field2': {'required': False,
u'max_length': 10, 'max_length': 10,
u'type': u'CharField', 'type': 'string',
u'read_only': False}} 'read_only': False}}
self.assertEqual(set(expected.keys()), set(humanized.keys())) self.assertEqual(expected, metadata)
for k, v in humanized.iteritems():
self.assertEqual(v, expected[k])

View File

@ -122,21 +122,24 @@ class TestRootView(TestCase):
], ],
'name': 'Root', 'name': 'Root',
'description': 'Example description for OPTIONS.', 'description': 'Example description for OPTIONS.',
'actions': {} 'actions': {
} 'POST': {
expected['actions']['GET'] = {} 'text': {
expected['actions']['POST'] = { 'max_length': 100,
'text': { 'read_only': False,
'max_length': 100, 'required': True,
'read_only': False, 'type': 'string',
'required': True, "label": "Text comes here",
'type': 'String', "help_text": "Text description."
}, },
'id': { 'id': {
'read_only': True, 'read_only': True,
'required': False, 'required': False,
'type': 'Integer', 'type': 'integer',
}, 'label': 'ID',
},
}
}
} }
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected) self.assertEqual(response.data, expected)
@ -239,9 +242,9 @@ class TestInstanceView(TestCase):
""" """
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
""" """
request = factory.options('/') request = factory.options('/1')
with self.assertNumQueries(0): with self.assertNumQueries(1):
response = self.view(request).render() response = self.view(request, pk=1).render()
expected = { expected = {
'parses': [ 'parses': [
'application/json', 'application/json',
@ -254,24 +257,25 @@ class TestInstanceView(TestCase):
], ],
'name': 'Instance', 'name': 'Instance',
'description': 'Example description for OPTIONS.', 'description': 'Example description for OPTIONS.',
'actions': {} 'actions': {
} 'PUT': {
for method in ('GET', 'DELETE'): 'text': {
expected['actions'][method] = {} 'max_length': 100,
for method in ('PATCH', 'PUT'): 'read_only': False,
expected['actions'][method] = { 'required': True,
'text': { 'type': 'string',
'max_length': 100, 'label': 'Text comes here',
'read_only': False, 'help_text': 'Text description.'
'required': True, },
'type': 'String', 'id': {
}, 'read_only': True,
'id': { 'required': False,
'read_only': True, 'type': 'integer',
'required': False, 'label': 'ID',
'type': 'Integer', },
}, }
} }
}
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected) self.assertEqual(response.data, expected)

View File

@ -114,44 +114,41 @@ class ModelPermissionsIntegrationTests(TestCase):
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) 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', request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.permitted_credentials) HTTP_AUTHORIZATION=self.permitted_credentials)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) 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): def test_options_disallowed(self):
request = factory.options('/', content_type='application/json', request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials) HTTP_AUTHORIZATION=self.disallowed_credentials)
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertNotIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
request = factory.options('/1', content_type='application/json', request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials) HTTP_AUTHORIZATION=self.disallowed_credentials)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertNotIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
def test_options_updateonly(self): def test_options_updateonly(self):
request = factory.options('/', content_type='application/json', request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials) HTTP_AUTHORIZATION=self.updateonly_credentials)
response = root_view(request, pk='1') response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) self.assertNotIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
request = factory.options('/1', content_type='application/json', request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials) HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1') response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data) 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): class OwnerModel(models.Model):

View File

@ -6,6 +6,7 @@ from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import unittest from django.utils import unittest
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions from rest_framework import status, permissions
from rest_framework.compat import yaml, etree, patterns, url, include from rest_framework.compat import yaml, etree, patterns, url, include
from rest_framework.response import Response from rest_framework.response import Response
@ -238,6 +239,13 @@ class JSONRendererTests(TestCase):
Tests specific to the JSON Renderer 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): def test_without_content_type_args(self):
""" """
Test basic JSON rendering. Test basic JSON rendering.

View File

@ -3,7 +3,8 @@ Helper classes for parsers.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.datastructures import SortedDict 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 from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
@ -19,7 +20,9 @@ class JSONEncoder(json.JSONEncoder):
def default(self, o): def default(self, o):
# For Date Time string spec, see ECMA 262 # For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 # 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() r = o.isoformat()
if o.microsecond: if o.microsecond:
r = r[:23] + r[26:] r = r[:23] + r[26:]

View File

@ -5,12 +5,11 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
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 View from rest_framework.compat import View
from rest_framework.fields import humanize_form_fields from rest_framework.request import Request
from rest_framework.request import clone_request, Request
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.formatting import get_view_name, get_view_description from rest_framework.utils.formatting import get_view_name, get_view_description
@ -54,53 +53,6 @@ class APIView(View):
'Vary': 'Accept' '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): def http_method_not_allowed(self, request, *args, **kwargs):
""" """
If `request.method` does not correspond to a handler method, If `request.method` does not correspond to a handler method,
@ -383,3 +335,15 @@ class APIView(View):
a less useful default implementation. a less useful default implementation.
""" """
return Response(self.metadata(request), status=status.HTTP_200_OK) 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