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`.
|
* 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.
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 ###
|
||||||
|
|
|
@ -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)):
|
||||||
|
|
|
@ -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])
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:]
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user