aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/fields.py59
-rw-r--r--rest_framework/generics.py43
-rw-r--r--rest_framework/serializers.py16
-rw-r--r--rest_framework/tests/fields.py54
-rw-r--r--rest_framework/tests/generics.py74
-rw-r--r--rest_framework/tests/permissions.py15
-rw-r--r--rest_framework/tests/renderers.py8
-rw-r--r--rest_framework/utils/encoders.py7
-rw-r--r--rest_framework/views.py64
9 files changed, 174 insertions, 166 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index cb5f9a40..1534eeca 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -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,14 +413,15 @@ class URLField(CharField):
class SlugField(CharField):
type_name = 'SlugField'
+ type_label = 'slug'
form_field_class = forms.SlugField
-
+
default_error_messages = {
'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
" underscores or hyphens."),
}
default_validators = [validators.validate_slug]
-
+
def __init__(self, *args, **kwargs):
super(SlugField, self).__init__(*args, **kwargs)
@@ -440,10 +431,11 @@ class SlugField(CharField):
#result.widget = copy.deepcopy(self.widget, memo)
result.validators = self.validators[:]
return result
-
-
+
+
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 = {
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 05ec93d3..afcb8a9f 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -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 ###
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 17da8c25..5be07fb7 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -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)):
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index 22c515a9..bff4400b 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -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
@@ -784,12 +780,12 @@ class SlugFieldTests(TestCase):
"""
class SlugFieldSerializer(serializers.ModelSerializer):
slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
-
+
class Meta:
model = self.SlugFieldModel
-
+
s = SlugFieldSerializer(data={'slug_field': 'a b'})
-
+
self.assertEqual(s.is_valid(), False)
self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
@@ -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)
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index a2f8fb4b..f091d0db 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -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)
diff --git a/rest_framework/tests/permissions.py b/rest_framework/tests/permissions.py
index 5a18182b..6caaf65b 100644
--- a/rest_framework/tests/permissions.py
+++ b/rest_framework/tests/permissions.py
@@ -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):
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 9096c82d..cf3f4b46 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -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.
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index b6de18a8..b26a2085 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -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:]
diff --git a/rest_framework/views.py b/rest_framework/views.py
index d1afbe89..e1b6705b 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -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