diff options
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/generics.py | 51 | ||||
| -rw-r--r-- | rest_framework/metadata.py | 126 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 8 | ||||
| -rw-r--r-- | rest_framework/settings.py | 2 | ||||
| -rw-r--r-- | rest_framework/utils/field_mapping.py | 20 | ||||
| -rw-r--r-- | rest_framework/views.py | 24 | 
6 files changed, 150 insertions, 81 deletions
| diff --git a/rest_framework/generics.py b/rest_framework/generics.py index eb6b64ef..f49b0a43 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -4,13 +4,11 @@ Generic views that provide commonly needed behaviour.  from __future__ import unicode_literals  from django.db.models.query import QuerySet -from django.core.exceptions import PermissionDenied  from django.core.paginator import Paginator, InvalidPage  from django.http import Http404  from django.shortcuts import get_object_or_404 as _get_object_or_404  from django.utils.translation import ugettext as _ -from rest_framework import views, mixins, exceptions -from rest_framework.request import clone_request +from rest_framework import views, mixins  from rest_framework.settings import api_settings @@ -249,53 +247,6 @@ class GenericAPIView(views.APIView):          return obj -    # The following are placeholder methods, -    # and are intended to be overridden. -    # -    # The are not called by GenericAPIView directly, -    # but are used by the mixin methods. -    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': -                    try: -                        self.get_object() -                    except Http404: -                        # Http404 should be acceptable and the serializer -                        # metadata should be populated. Except this so the -                        # outer "else" clause of the try-except-else block -                        # will be executed. -                        pass -            except (exceptions.APIException, PermissionDenied): -                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  # by composing the mixin classes with the base view. diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py new file mode 100644 index 00000000..580259de --- /dev/null +++ b/rest_framework/metadata.py @@ -0,0 +1,126 @@ +""" +The metadata API is used to allow cusomization of how `OPTIONS` requests +are handled. We currently provide a single default implementation that returns +some fairly ad-hoc information about the view. + +Future implementations might use JSON schema or other definations in order +to return this information in a more standardized way. +""" +from __future__ import unicode_literals + +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.utils import six +from django.utils.datastructures import SortedDict +from rest_framework import exceptions, serializers +from rest_framework.compat import force_text +from rest_framework.request import clone_request +from rest_framework.utils.field_mapping import ClassLookupDict + + +class BaseMetadata(object): +    def determine_metadata(self, request, view): +        """ +        Return a dictionary of metadata about the view. +        Used to return responses for OPTIONS requests. +        """ +        raise NotImplementedError(".determine_metadata() must be overridden.") + + +class SimpleMetadata(BaseMetadata): +    """ +    This is the default metadata implementation. +    It returns an ad-hoc set of information about the view. +    There are not any formalized standards for `OPTIONS` responses +    for us to base this on. +    """ +    label_lookup = ClassLookupDict({ +        serializers.Field: 'field', +        serializers.BooleanField: 'boolean', +        serializers.CharField: 'string', +        serializers.URLField: 'url', +        serializers.EmailField: 'email', +        serializers.RegexField: 'regex', +        serializers.SlugField: 'slug', +        serializers.IntegerField: 'integer', +        serializers.FloatField: 'float', +        serializers.DecimalField: 'decimal', +        serializers.DateField: 'date', +        serializers.DateTimeField: 'datetime', +        serializers.TimeField: 'time', +        serializers.ChoiceField: 'choice', +        serializers.MultipleChoiceField: 'multiple choice', +        serializers.FileField: 'file upload', +        serializers.ImageField: 'image upload', +    }) + +    def determine_metadata(self, request, view): +        metadata = SortedDict() +        metadata['name'] = view.get_view_name() +        metadata['description'] = view.get_view_description() +        metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes] +        metadata['parses'] = [parser.media_type for parser in view.parser_classes] +        if hasattr(view, 'get_serializer'): +            actions = self.determine_actions(request, view) +            if actions: +                metadata['actions'] = actions +        return metadata + +    def determine_actions(self, request, view): +        """ +        For generic class based views we return information about +        the fields that are accepted for 'PUT' and 'POST' methods. +        """ +        actions = {} +        for method in set(['PUT', 'POST']) & set(view.allowed_methods): +            view.request = clone_request(request, method) +            try: +                # Test global permissions +                if hasattr(view, 'check_permissions'): +                    view.check_permissions(view.request) +                # Test object permissions +                if method == 'PUT' and hasattr(view, 'get_object'): +                    view.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 = view.get_serializer() +                actions[method] = self.get_serializer_info(serializer) +            finally: +                view.request = request + +        return actions + +    def get_serializer_info(self, serializer): +        """ +        Given an instance of a serializer, return a dictionary of metadata +        about its fields. +        """ +        return SortedDict([ +            (field_name, self.get_field_info(field)) +            for field_name, field in six.iteritems(serializer.fields) +        ]) + +    def get_field_info(self, field): +        """ +        Given an instance of a serializer field, return a dictionary +        of metadata about it. +        """ +        field_info = SortedDict() +        field_info['type'] = self.label_lookup[field] +        field_info['required'] = getattr(field, 'required', False) + +        for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']: +            value = getattr(field, attr, None) +            if value is not None and value != '': +                field_info[attr] = force_text(value, strings_only=True) + +        if hasattr(field, 'choices'): +            field_info['choices'] = [ +                {'value': choice_value, 'display_name': choice_name} +                for choice_value, choice_name in field.choices.items() +            ] + +        return field_info diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d8d72a4c..8902294b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -21,7 +21,7 @@ from rest_framework.utils import html, model_meta, representation  from rest_framework.utils.field_mapping import (      get_url_kwargs, get_field_kwargs,      get_relation_kwargs, get_nested_relation_kwargs, -    lookup_class +    ClassLookupDict  )  import copy  import inspect @@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer):  class ModelSerializer(Serializer): -    _field_mapping = { +    _field_mapping = ClassLookupDict({          models.AutoField: IntegerField,          models.BigIntegerField: IntegerField,          models.BooleanField: BooleanField, @@ -341,7 +341,7 @@ class ModelSerializer(Serializer):          models.TextField: CharField,          models.TimeField: TimeField,          models.URLField: URLField, -    } +    })      _related_class = PrimaryKeyRelatedField      def create(self, attrs): @@ -400,7 +400,7 @@ class ModelSerializer(Serializer):              elif field_name in info.fields_and_pk:                  # Create regular model fields.                  model_field = info.fields_and_pk[field_name] -                field_cls = lookup_class(self._field_mapping, model_field) +                field_cls = self._field_mapping[model_field]                  kwargs = get_field_kwargs(field_name, model_field)                  if 'choices' in kwargs:                      # Fields with choices get coerced into `ChoiceField` diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 421e146c..d7fb0a43 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -45,6 +45,7 @@ DEFAULTS = {      ),      'DEFAULT_THROTTLE_CLASSES': (),      'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', +    'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',      # Genric view behavior      'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', @@ -121,6 +122,7 @@ IMPORT_STRINGS = (      'DEFAULT_PERMISSION_CLASSES',      'DEFAULT_THROTTLE_CLASSES',      'DEFAULT_CONTENT_NEGOTIATION_CLASS', +    'DEFAULT_METADATA_CLASS',      'DEFAULT_MODEL_SERIALIZER_CLASS',      'DEFAULT_PAGINATION_SERIALIZER_CLASS',      'DEFAULT_FILTER_BACKENDS', diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index c208afdc..c3794083 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -9,17 +9,21 @@ from rest_framework.compat import clean_manytomany_helptext  import inspect -def lookup_class(mapping, instance): +class ClassLookupDict(object):      """ -    Takes a dictionary with classes as keys, and an object. -    Traverses the object's inheritance hierarchy in method -    resolution order, and returns the first matching value +    Takes a dictionary with classes as keys. +    Lookups against this object will traverses the object's inheritance +    hierarchy in method resolution order, and returns the first matching value      from the dictionary or raises a KeyError if nothing matches.      """ -    for cls in inspect.getmro(instance.__class__): -        if cls in mapping: -            return mapping[cls] -    raise KeyError('Class %s not found in lookup.', cls.__name__) +    def __init__(self, mapping): +        self.mapping = mapping + +    def __getitem__(self, key): +        for cls in inspect.getmro(key.__class__): +            if cls in self.mapping: +                return self.mapping[cls] +        raise KeyError('Class %s not found in lookup.', cls.__name__)  def needs_label(model_field, field_name): diff --git a/rest_framework/views.py b/rest_framework/views.py index 9f08a4ad..835e223a 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals  from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS  from django.http import Http404 -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 smart_text, HttpResponseBase, View @@ -99,6 +98,7 @@ class APIView(View):      throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES      permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES      content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS +    metadata_class = api_settings.DEFAULT_METADATA_CLASS      # Allow dependancy injection of other settings to make testing easier.      settings = api_settings @@ -418,22 +418,8 @@ class APIView(View):      def options(self, request, *args, **kwargs):          """          Handler method for HTTP 'OPTIONS' request. -        We may as well implement this as Django will otherwise provide -        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. -        """ -        # By default we can't provide any form-like information, however the -        # generic views override this implementation and add additional -        # information for POST and PUT methods, based on the serializer. -        ret = SortedDict() -        ret['name'] = self.get_view_name() -        ret['description'] = self.get_view_description() -        ret['renders'] = [renderer.media_type for renderer in self.renderer_classes] -        ret['parses'] = [parser.media_type for parser in self.parser_classes] -        return ret +        if self.metadata_class is None: +            return self.http_method_not_allowed(request, *args, **kwargs) +        data = self.metadata_class().determine_metadata(request, self) +        return Response(data, status=status.HTTP_200_OK) | 
