diff options
Diffstat (limited to 'rest_framework/renderers.py')
| -rw-r--r-- | rest_framework/renderers.py | 249 |
1 files changed, 188 insertions, 61 deletions
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 748ebac9..634338e9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -12,18 +12,26 @@ import json import django from django import forms from django.core.exceptions import ImproperlyConfigured +from django.core.paginator import Page 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.encoding import smart_text from django.utils.xmlutils import SimplerXMLGenerator -from rest_framework.compat import StringIO, smart_text, yaml +from django.utils.six.moves import StringIO +from rest_framework import exceptions, serializers, status, VERSION +from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, 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 +52,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 +70,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 +90,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, @@ -92,18 +103,15 @@ class JSONRenderer(BaseRenderer): # and may (or may not) be unicode. # On python 3.x json.dumps() returns unicode strings. if isinstance(ret, six.text_type): + # We always fully escape \u2028 and \u2029 to ensure we output JSON + # that is a strict javascript subset. If bytes were returned + # by json.dumps() then we don't have these characters in any case. + # See: http://timelessrepo.com/json-isnt-a-javascript-subset + ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029') return bytes(ret.encode('utf-8')) 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 +129,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 +204,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 +218,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. @@ -288,7 +288,9 @@ class TemplateHTMLRenderer(BaseRenderer): return view.get_template_names() elif hasattr(view, 'template_name'): return [view.template_name] - raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response') + raise ImproperlyConfigured( + 'Returned a template response with no `template_name` attribute set on either the view or response' + ) def get_exception_template(self, response): template_names = [name % {'status_code': response.status_code} @@ -347,18 +349,113 @@ 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.FileField: { + 'base_template': 'input.html', + 'input_type': 'file' + }, + 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 +523,37 @@ 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) + if isinstance(instance, Page): + 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 + kwargs = {'data': request.data} except ParseError: - data = None - files = None + kwargs = {} + existing_serializer = serializer else: - data = None - files = None + kwargs = {} + 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 +565,42 @@ 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, **kwargs) + else: + serializer = view.get_serializer(**kwargs) + + if hasattr(serializer, 'initial_data'): + 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) + if isinstance(instance, Page): + 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 +610,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 +620,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 +680,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 +706,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, |
