diff options
Diffstat (limited to 'rest_framework/renderers.py')
| -rw-r--r-- | rest_framework/renderers.py | 227 | 
1 files changed, 168 insertions, 59 deletions
| diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 748ebac9..e87d16d0 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -13,17 +13,24 @@ import django  from django import forms  from django.core.exceptions import ImproperlyConfigured  from django.http.multipartparser import parse_header -from django.template import RequestContext, loader, Template +from django.template import Context, RequestContext, loader, Template  from django.test.client import encode_multipart  from django.utils import six  from django.utils.xmlutils import SimplerXMLGenerator -from rest_framework.compat import StringIO, smart_text, yaml +from rest_framework import exceptions, serializers, status, VERSION +from rest_framework.compat import ( +    SHORT_SEPARATORS, LONG_SEPARATORS, StringIO, smart_text, yaml +)  from rest_framework.exceptions import ParseError  from rest_framework.settings import api_settings  from rest_framework.request import is_form_media_type, override_method  from rest_framework.utils import encoders  from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework import exceptions, status, VERSION +from rest_framework.utils.field_mapping import ClassLookupDict + + +def zero_as_none(value): +    return None if value == 0 else value  class BaseRenderer(object): @@ -44,13 +51,13 @@ class BaseRenderer(object):  class JSONRenderer(BaseRenderer):      """      Renderer which serializes to JSON. -    Applies JSON's backslash-u character escaping for non-ascii characters.      """      media_type = 'application/json'      format = 'json'      encoder_class = encoders.JSONEncoder -    ensure_ascii = True +    ensure_ascii = not api_settings.UNICODE_JSON +    compact = api_settings.COMPACT_JSON      # We don't set a charset because JSON is a binary encoding,      # that can be encoded as utf-8, utf-16 or utf-32. @@ -62,9 +69,10 @@ class JSONRenderer(BaseRenderer):          if accepted_media_type:              # If the media type looks like 'application/json; indent=4',              # then pretty print the result. +            # Note that we coerce `indent=0` into `indent=None`.              base_media_type, params = parse_header(accepted_media_type.encode('ascii'))              try: -                return max(min(int(params['indent']), 8), 0) +                return zero_as_none(max(min(int(params['indent']), 8), 0))              except (KeyError, ValueError, TypeError):                  pass @@ -81,10 +89,12 @@ class JSONRenderer(BaseRenderer):          renderer_context = renderer_context or {}          indent = self.get_indent(accepted_media_type, renderer_context) +        separators = SHORT_SEPARATORS if (indent is None and self.compact) else LONG_SEPARATORS          ret = json.dumps(              data, cls=self.encoder_class, -            indent=indent, ensure_ascii=self.ensure_ascii +            indent=indent, ensure_ascii=self.ensure_ascii, +            separators=separators          )          # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, @@ -96,14 +106,6 @@ class JSONRenderer(BaseRenderer):          return ret -class UnicodeJSONRenderer(JSONRenderer): -    ensure_ascii = False -    """ -    Renderer which serializes to JSON. -    Does *not* apply JSON's character escaping for non-ascii characters. -    """ - -  class JSONPRenderer(JSONRenderer):      """      Renderer which serializes to json, @@ -121,7 +123,7 @@ class JSONPRenderer(JSONRenderer):          Determine the name of the callback to wrap around the json output.          """          request = renderer_context.get('request', None) -        params = request and request.QUERY_PARAMS or {} +        params = request and request.query_params or {}          return params.get(self.callback_parameter, self.default_callback)      def render(self, data, accepted_media_type=None, renderer_context=None): @@ -196,7 +198,7 @@ class YAMLRenderer(BaseRenderer):      format = 'yaml'      encoder = encoders.SafeDumper      charset = 'utf-8' -    ensure_ascii = True +    ensure_ascii = False      def render(self, data, accepted_media_type=None, renderer_context=None):          """ @@ -210,14 +212,6 @@ class YAMLRenderer(BaseRenderer):          return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii) -class UnicodeYAMLRenderer(YAMLRenderer): -    """ -    Renderer which serializes to YAML. -    Does *not* apply character escaping for non-ascii characters. -    """ -    ensure_ascii = False - -  class TemplateHTMLRenderer(BaseRenderer):      """      An HTML renderer for use with templates. @@ -347,18 +341,109 @@ class HTMLFormRenderer(BaseRenderer):      """      media_type = 'text/html'      format = 'form' -    template = 'rest_framework/form.html'      charset = 'utf-8' +    template_pack = 'rest_framework/horizontal/' +    base_template = 'form.html' + +    default_style = ClassLookupDict({ +        serializers.Field: { +            'base_template': 'input.html', +            'input_type': 'text' +        }, +        serializers.EmailField: { +            'base_template': 'input.html', +            'input_type': 'email' +        }, +        serializers.URLField: { +            'base_template': 'input.html', +            'input_type': 'url' +        }, +        serializers.IntegerField: { +            'base_template': 'input.html', +            'input_type': 'number' +        }, +        serializers.DateTimeField: { +            'base_template': 'input.html', +            'input_type': 'datetime-local' +        }, +        serializers.DateField: { +            'base_template': 'input.html', +            'input_type': 'date' +        }, +        serializers.TimeField: { +            'base_template': 'input.html', +            'input_type': 'time' +        }, +        serializers.BooleanField: { +            'base_template': 'checkbox.html' +        }, +        serializers.ChoiceField: { +            'base_template': 'select.html',  # Also valid: 'radio.html' +        }, +        serializers.MultipleChoiceField: { +            'base_template': 'select_multiple.html',  # Also valid: 'checkbox_multiple.html' +        }, +        serializers.RelatedField: { +            'base_template': 'select.html',  # Also valid: 'radio.html' +        }, +        serializers.ManyRelatedField: { +            'base_template': 'select_multiple.html',  # Also valid: 'checkbox_multiple.html' +        }, +        serializers.Serializer: { +            'base_template': 'fieldset.html' +        }, +        serializers.ListSerializer: { +            'base_template': 'list_fieldset.html' +        } +    }) + +    def render_field(self, field, parent_style): +        style = dict(self.default_style[field]) +        style.update(field.style) +        if 'template_pack' not in style: +            style['template_pack'] = parent_style.get('template_pack', self.template_pack) +        style['renderer'] = self + +        if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type): +            field.value = field.value.rstrip('Z') + +        if 'template' in style: +            template_name = style['template'] +        else: +            template_name = style['template_pack'].strip('/') + '/' + style['base_template'] + +        template = loader.get_template(template_name) +        context = Context({'field': field, 'style': style}) +        return template.render(context)      def render(self, data, accepted_media_type=None, renderer_context=None):          """          Render serializer data and return an HTML form, as a string.          """ +        form = data.serializer +        meta = getattr(form, 'Meta', None) +        style = getattr(meta, 'style', {}) +        if 'template_pack' not in style: +            style['template_pack'] = self.template_pack +        if 'base_template' not in style: +            style['base_template'] = self.base_template +        style['renderer'] = self + +        # This API needs to be finessed and finalized for 3.1 +        if 'template' in renderer_context: +            template_name = renderer_context['template'] +        elif 'template' in style: +            template_name = style['template'] +        else: +            template_name = style['template_pack'].strip('/') + '/' + style['base_template'] +          renderer_context = renderer_context or {}          request = renderer_context['request'] - -        template = loader.get_template(self.template) -        context = RequestContext(request, {'form': data}) +        template = loader.get_template(template_name) +        context = RequestContext(request, { +            'form': form, +            'style': style +        })          return template.render(context) @@ -426,27 +511,35 @@ class BrowsableAPIRenderer(BaseRenderer):              return False  # Doesn't have permissions          return True -    def get_rendered_html_form(self, view, method, request): +    def get_rendered_html_form(self, data, view, method, request):          """          Return a string representing a rendered HTML form, possibly bound to          either the input or output data.          In the absence of the View having an associated form then return None.          """ -        if request.method == method: +        # See issue #2089 for refactoring this. +        serializer = getattr(data, 'serializer', None) +        if serializer and not getattr(serializer, 'many', False): +            instance = getattr(serializer, 'instance', None) +        else: +            instance = None + +        # If this is valid serializer data, and the form is for the same +        # HTTP method as was used in the request then use the existing +        # serializer instance, rather than dynamically creating a new one. +        if request.method == method and serializer is not None:              try: -                data = request.DATA -                files = request.FILES +                data = request.data              except ParseError:                  data = None -                files = None +            existing_serializer = serializer          else:              data = None -            files = None +            existing_serializer = None          with override_method(view, request, method) as request: -            obj = getattr(view, 'object', None) -            if not self.show_form_for_method(view, method, request, obj): +            if not self.show_form_for_method(view, method, request, instance):                  return              if method in ('DELETE', 'OPTIONS'): @@ -458,19 +551,38 @@ class BrowsableAPIRenderer(BaseRenderer):              ):                  return -            serializer = view.get_serializer(instance=obj, data=data, files=files) -            serializer.is_valid() -            data = serializer.data - +            if existing_serializer is not None: +                serializer = existing_serializer +            else: +                if method in ('PUT', 'PATCH'): +                    serializer = view.get_serializer(instance=instance, data=data) +                else: +                    serializer = view.get_serializer(data=data) +                if data is not None: +                    serializer.is_valid()              form_renderer = self.form_renderer_class() -            return form_renderer.render(data, self.accepted_media_type, self.renderer_context) - -    def get_raw_data_form(self, view, method, request): +            return form_renderer.render( +                serializer.data, +                self.accepted_media_type, +                dict( +                    list(self.renderer_context.items()) + +                    [('template', 'rest_framework/api_form.html')] +                ) +            ) + +    def get_raw_data_form(self, data, view, method, request):          """          Returns a form that allows for arbitrary content types to be tunneled          via standard HTML forms.          (Which are typically application/x-www-form-urlencoded)          """ +        # See issue #2089 for refactoring this. +        serializer = getattr(data, 'serializer', None) +        if serializer and not getattr(serializer, 'many', False): +            instance = getattr(serializer, 'instance', None) +        else: +            instance = None +          with override_method(view, request, method) as request:              # If we're not using content overloading there's no point in              # supplying a generic form, as the view won't treat the form's @@ -480,8 +592,7 @@ class BrowsableAPIRenderer(BaseRenderer):                  return None              # Check permissions -            obj = getattr(view, 'object', None) -            if not self.show_form_for_method(view, method, request, obj): +            if not self.show_form_for_method(view, method, request, instance):                  return              # If possible, serialize the initial content for the generic form @@ -491,12 +602,10 @@ class BrowsableAPIRenderer(BaseRenderer):                  # View has a serializer defined and parser class has a                  # corresponding renderer that can be used to render the data. -                # Get a read-only version of the serializer -                serializer = view.get_serializer(instance=obj) -                if obj is None: -                    for name, field in serializer.fields.items(): -                        if getattr(field, 'read_only', None): -                            del serializer.fields[name] +                if method in ('PUT', 'PATCH'): +                    serializer = view.get_serializer(instance=instance) +                else: +                    serializer = view.get_serializer()                  # Render the raw data content                  renderer = renderer_class() @@ -553,9 +662,9 @@ class BrowsableAPIRenderer(BaseRenderer):          renderer = self.get_default_renderer(view) -        raw_data_post_form = self.get_raw_data_form(view, 'POST', request) -        raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) -        raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) +        raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request) +        raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request) +        raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)          raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form          response_headers = dict(response.items()) @@ -579,10 +688,10 @@ class BrowsableAPIRenderer(BaseRenderer):              'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],              'response_headers': response_headers, -            'put_form': self.get_rendered_html_form(view, 'PUT', request), -            'post_form': self.get_rendered_html_form(view, 'POST', request), -            'delete_form': self.get_rendered_html_form(view, 'DELETE', request), -            'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), +            'put_form': self.get_rendered_html_form(data, view, 'PUT', request), +            'post_form': self.get_rendered_html_form(data, view, 'POST', request), +            'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request), +            'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),              'raw_data_put_form': raw_data_put_form,              'raw_data_post_form': raw_data_post_form, | 
