diff options
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/fields.py | 59 | ||||
| -rw-r--r-- | rest_framework/generics.py | 43 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 16 | ||||
| -rw-r--r-- | rest_framework/tests/fields.py | 54 | ||||
| -rw-r--r-- | rest_framework/tests/generics.py | 74 | ||||
| -rw-r--r-- | rest_framework/tests/permissions.py | 15 | ||||
| -rw-r--r-- | rest_framework/tests/renderers.py | 8 | ||||
| -rw-r--r-- | rest_framework/utils/encoders.py | 7 | ||||
| -rw-r--r-- | rest_framework/views.py | 64 | 
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 | 
