aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--rest_framework/fields.py91
-rw-r--r--rest_framework/tests/fields.py78
-rw-r--r--rest_framework/tests/generics.py42
-rw-r--r--rest_framework/tests/permissions.py45
-rw-r--r--rest_framework/views.py50
5 files changed, 293 insertions, 13 deletions
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index b5f99823..d6db3ebe 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -23,13 +23,44 @@ 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
+from rest_framework.compat import (timezone, parse_date, parse_datetime,
+ parse_time)
from rest_framework.compat import BytesIO
from rest_framework.compat import six
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
from rest_framework.settings import api_settings
+HUMANIZED_FIELD_TYPES = {
+ 'BooleanField': u'Boolean',
+ 'CharField': u'Single Character',
+ 'ChoiceField': u'Single Choice',
+ 'ComboField': u'Single Choice',
+ 'DateField': u'Date',
+ 'DateTimeField': u'Date and Time',
+ 'DecimalField': u'Decimal',
+ 'EmailField': u'Email',
+ 'Field': u'Field',
+ 'FileField': u'File',
+ 'FilePathField': u'File Path',
+ 'FloatField': u'Float',
+ 'GenericIPAddressField': u'Generic IP Address',
+ 'IPAddressField': u'IP Address',
+ 'ImageField': u'Image',
+ 'IntegerField': u'Integer',
+ 'MultiValueField': u'Multiple Value',
+ 'MultipleChoiceField': u'Multiple Choice',
+ 'NullBooleanField': u'Nullable Boolean',
+ 'RegexField': u'Regular Expression',
+ 'SlugField': u'Slug',
+ 'SplitDateTimeField': u'Split Date and Time',
+ 'TimeField': u'Time',
+ 'TypedChoiceField': u'Typed Single Choice',
+ 'TypedMultipleChoiceField': u'Typed Multiple Choice',
+ 'URLField': u'URL',
+}
+
+
def is_simple_callable(obj):
"""
True if the object is a callable that takes no arguments.
@@ -61,7 +92,8 @@ def get_component(obj, attr_name):
def readable_datetime_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
+ format = ', '.join(formats).replace(ISO_8601,
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
return humanize_strptime(format)
@@ -70,6 +102,61 @@ def readable_date_formats(formats):
return humanize_strptime(format)
+def humanize_field_type(field_type):
+ """Return a human-readable name for a field type.
+
+ :param field_type: Either a field type class (for example
+ django.forms.fields.DateTimeField), or the name of a field type
+ (for example "DateTimeField").
+
+ :return: unicode
+
+ """
+ if isinstance(field_type, basestring):
+ field_type_name = field_type
+ else:
+ field_type_name = field_type.__name__
+ try:
+ return HUMANIZED_FIELD_TYPES[field_type_name]
+ except KeyError:
+ humanized = re.sub('([a-z0-9])([A-Z])', r'\1 \2', field_type_name)
+ return humanized.capitalize()
+
+
+def humanize_field(field):
+ """Return a human-readable description of a field.
+
+ :param field: A Django field.
+
+ :return: A dictionary of the form {type: type name, required: bool,
+ label: field label: read_only: bool,
+ help_text: optional help text}
+
+ """
+ humanized = {
+ 'type': humanize_field_type(field.__class__),
+ 'required': getattr(field, 'required', False),
+ 'label': field.label,
+ }
+ optional_attrs = ['read_only', 'help_text']
+ for attr in optional_attrs:
+ if hasattr(field, attr):
+ humanized[attr] = getattr(field, attr)
+ return humanized
+
+
+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)
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index dad69975..7a5ed718 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -4,6 +4,9 @@ General serializer field tests.
from __future__ import unicode_literals
from django.utils.datastructures import SortedDict
import datetime
+from rest_framework.fields import (humanize_field, humanize_field_type,
+ humanize_form_fields)
+from django import forms
from decimal import Decimal
from django.db import models
from django.test import TestCase
@@ -11,6 +14,9 @@ from django.core import validators
from rest_framework import serializers
from rest_framework.serializers import Serializer
from rest_framework.tests.models import RESTFrameworkModel
+from rest_framework.fields import Field
+from collections import namedtuple
+from uuid import uuid4
class TimestampedModel(models.Model):
@@ -809,3 +815,75 @@ class URLFieldTests(TestCase):
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
+
+
+class HumanizedFieldType(TestCase):
+ def test_standard_type_classes(self):
+ for field_type_name in forms.fields.__all__:
+ field_type = getattr(forms.fields, field_type_name)
+ humanized = humanize_field_type(field_type)
+ self.assert_valid_name(humanized)
+
+ def test_standard_type_names(self):
+ for field_type_name in forms.fields.__all__:
+ humanized = humanize_field_type(field_type_name)
+ self.assert_valid_name(humanized)
+
+ def test_custom_type_name(self):
+ humanized = humanize_field_type('SomeCustomType')
+ self.assertEquals(humanized, u'Some custom type')
+
+ def test_custom_type(self):
+ custom_type = namedtuple('SomeCustomType', [])
+ humanized = humanize_field_type(custom_type)
+ self.assertEquals(humanized, u'Some custom type')
+
+ def assert_valid_name(self, humanized):
+ """A humanized field name is valid if it's a non-empty
+ unicode.
+
+ """
+ self.assertIsInstance(humanized, unicode)
+ self.assertTrue(humanized)
+
+
+class HumanizedField(TestCase):
+ def setUp(self):
+ self.required_field = Field()
+ self.required_field.label = uuid4().hex
+ self.required_field.required = True
+
+ self.optional_field = Field()
+ self.optional_field.label = uuid4().hex
+ self.optional_field.required = False
+
+ def test_required(self):
+ self.assertEqual(humanize_field(self.required_field)['required'], True)
+
+ def test_optional(self):
+ self.assertEqual(humanize_field(self.optional_field)['required'],
+ False)
+
+ def test_label(self):
+ for field in (self.required_field, self.optional_field):
+ self.assertEqual(humanize_field(field)['label'], field.label)
+
+
+class Form(forms.Form):
+ field1 = forms.CharField(max_length=3, label='field one')
+ field2 = forms.CharField(label='field two')
+
+
+class HumanizedSerializer(TestCase):
+ def setUp(self):
+ self.serializer = TimestampedModelSerializer()
+
+ def test_humanized(self):
+ humanized = humanize_form_fields(Form())
+ self.assertEqual(humanized, {
+ 'field1': {
+ u'help_text': u'', u'required': True,
+ u'type': u'Single Character', u'label': 'field one'},
+ 'field2': {
+ u'help_text': u'', u'required': True,
+ u'type': u'Single Character', u'label': 'field two'}})
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index 15d87e86..d8556638 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -121,8 +121,27 @@ class TestRootView(TestCase):
'text/html'
],
'name': 'Root',
- 'description': 'Example description for OPTIONS.'
+ 'description': 'Example description for OPTIONS.',
+ 'actions': {}
}
+ # TODO: this is just a draft for fields' metadata - needs review and decision
+ for method in ('GET', 'POST',):
+ expected['actions'][method] = {
+ 'text': {
+ 'description': '',
+ 'label': '',
+ 'readonly': False,
+ 'required': True,
+ 'type': 'CharField',
+ },
+ 'id': {
+ 'description': '',
+ 'label': '',
+ 'readonly': True,
+ 'required': True,
+ 'type': 'IntegerField',
+ },
+ }
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected)
@@ -238,8 +257,27 @@ class TestInstanceView(TestCase):
'text/html'
],
'name': 'Instance',
- 'description': 'Example description for OPTIONS.'
+ 'description': 'Example description for OPTIONS.',
+ 'actions': {}
}
+ # TODO: this is just a draft idea for fields' metadata - needs review and decision
+ for method in ('GET', 'PATCH', 'PUT', 'DELETE'):
+ expected['actions'][method] = {
+ 'text': {
+ 'description': '',
+ 'label': '',
+ 'readonly': False,
+ 'required': True,
+ 'type': 'CharField',
+ },
+ 'id': {
+ 'description': '',
+ 'label': '',
+ 'readonly': True,
+ 'required': True,
+ 'type': 'IntegerField',
+ },
+ }
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 b3993be5..5a18182b 100644
--- a/rest_framework/tests/permissions.py
+++ b/rest_framework/tests/permissions.py
@@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ def test_options_permitted(self):
+ request = factory.options('/', content_type='application/json',
+ HTTP_AUTHORIZATION=self.permitted_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(), ['POST', 'GET',])
+
+ 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',])
+
+ 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',])
+
+ 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',])
+
+ 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',])
+
+ 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',])
+
class OwnerModel(models.Model):
text = models.CharField(max_length=100)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 555fa2f4..11d50e5d 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -8,7 +8,7 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
from rest_framework.compat import View
from rest_framework.response import Response
-from rest_framework.request import Request
+from rest_framework.request import clone_request, Request
from rest_framework.settings import api_settings
from rest_framework.utils.formatting import get_view_name, get_view_description
@@ -52,19 +52,51 @@ class APIView(View):
}
def metadata(self, request):
- return {
+ 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],
}
- # TODO: Add 'fields', from serializer info, if it exists.
- # serializer = self.get_serializer()
- # if serializer is not None:
- # field_name_types = {}
- # for name, field in form.fields.iteritems():
- # field_name_types[name] = field.__class__.__name__
- # content['fields'] = field_name_types
+ action_metadata = self._generate_action_metadata(request)
+ if action_metadata is not None:
+ content['actions'] = action_metadata
+
+ return content
+
+ def _generate_action_metadata(self, request):
+ '''
+ Helper for generating the fields metadata for allowed and permitted methods.
+ '''
+ 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: find right placement - APIView does not have get_serializer
+ serializer = self.get_serializer()
+ if serializer is not None:
+ field_name_types = {}
+ for name, field in serializer.fields.iteritems():
+ from rest_framework.fields import humanize_field
+ humanize_field(field)
+ field_name_types[name] = field.__class__.__name__
+
+ actions[method] = field_name_types
+ 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):
"""