diff options
Diffstat (limited to 'rest_framework/renderers.py')
| -rw-r--r-- | rest_framework/renderers.py | 188 |
1 files changed, 139 insertions, 49 deletions
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 94d253c9..22fd6e74 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1,14 +1,16 @@ """ -Renderers are used to serialize a View's output into specific media types. +Renderers are used to serialize a response into specific media types. -Django REST framework also provides HTML and PlainText renderers that help self-document the API, -by serializing the output along with documentation regarding the View, output status and headers, -and providing forms and links depending on the allowed methods, renderers and parsers on the View. +They give us a generic way of being able to handle various media types +on the response, such as JSON encoded data or HTML output. + +REST framework also provides an HTML renderer the renders the browseable API. """ +import copy import string from django import forms from django.http.multipartparser import parse_header -from django.template import RequestContext, loader +from django.template import RequestContext, loader, Template from django.utils import simplejson as json from rest_framework.compat import yaml from rest_framework.exceptions import ConfigurationError @@ -23,8 +25,8 @@ from rest_framework import serializers, parsers class BaseRenderer(object): """ - All renderers must extend this class, set the :attr:`media_type` attribute, - and override the :meth:`render` method. + All renderers should extend this class, setting the `media_type` + and `format` attributes, and override the `.render()` method. """ media_type = None @@ -98,7 +100,7 @@ class JSONPRenderer(JSONRenderer): callback = self.get_callback(renderer_context) json = super(JSONPRenderer, self).render(data, accepted_media_type, renderer_context) - return "%s(%s);" % (callback, json) + return u"%s(%s);" % (callback, json) class XMLRenderer(BaseRenderer): @@ -137,18 +139,33 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) -class HTMLRenderer(BaseRenderer): +class TemplateHTMLRenderer(BaseRenderer): """ - A Base class provided for convenience. + An HTML renderer for use with templates. + + The data supplied to the Response object should be a dictionary that will + be used as context for the template. + + The template name is determined by (in order of preference): + + 1. An explicit `.template_name` attribute set on the response. + 2. An explicit `.template_name` attribute set on this class. + 3. The return result of calling `view.get_template_names()`. - Render the object simply by using the given template. - To create a template renderer, subclass this class, and set - the :attr:`media_type` and :attr:`template` attributes. + For example: + data = {'users': User.objects.all()} + return Response(data, template_name='users.html') + + For pre-rendered HTML, see StaticHTMLRenderer. """ media_type = 'text/html' format = 'html' template_name = None + exception_template_names = [ + '%(status_code)s.html', + 'api_exception.html' + ] def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -165,15 +182,21 @@ class HTMLRenderer(BaseRenderer): request = renderer_context['request'] response = renderer_context['response'] - template_names = self.get_template_names(response, view) - template = self.resolve_template(template_names) - context = self.resolve_context(data, request) + if response.exception: + template = self.get_exception_template(response) + else: + template_names = self.get_template_names(response, view) + template = self.resolve_template(template_names) + + context = self.resolve_context(data, request, response) return template.render(context) def resolve_template(self, template_names): return loader.select_template(template_names) - def resolve_context(self, data, request): + def resolve_context(self, data, request, response): + if response.exception: + data['status_code'] = response.status_code return RequestContext(request, data) def get_template_names(self, response, view): @@ -185,6 +208,48 @@ class HTMLRenderer(BaseRenderer): return view.get_template_names() raise ConfigurationError('Returned a template response with no template_name') + def get_exception_template(self, response): + template_names = [name % {'status_code': response.status_code} + for name in self.exception_template_names] + + try: + # Try to find an appropriate error template + return self.resolve_template(template_names) + except: + # Fall back to using eg '404 Not Found' + return Template('%d %s' % (response.status_code, + response.status_text.title())) + + +# Note, subclass TemplateHTMLRenderer simply for the exception behavior +class StaticHTMLRenderer(TemplateHTMLRenderer): + """ + An HTML renderer class that simply returns pre-rendered HTML. + + The data supplied to the Response object should be a string representing + the pre-rendered HTML content. + + For example: + data = '<html><body>example</body></html>' + return Response(data) + + For template rendered HTML, see TemplateHTMLRenderer. + """ + media_type = 'text/html' + format = 'html' + + def render(self, data, accepted_media_type=None, renderer_context=None): + renderer_context = renderer_context or {} + response = renderer_context['response'] + + if response and response.exception: + request = renderer_context['request'] + template = self.get_exception_template(response) + context = self.resolve_context(data, request, response) + return template.render(context) + + return data + class BrowsableAPIRenderer(BaseRenderer): """ @@ -222,11 +287,9 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def get_form(self, view, method, request): + def show_form_for_method(self, view, method, request, obj): """ - Get a form, possibly bound to either the input or output data. - In the absence on of the Resource having an associated form then - provide a form that can be used to submit arbitrary content. + Returns True if a form should be shown for this method. """ if not method in view.allowed_methods: return # Not a valid method @@ -236,24 +299,13 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request): + if not view.has_permission(request, obj): return # Don't have permission except: return # Don't have permission and exception explicitly raise + return True - if method == 'DELETE' or method == 'OPTIONS': - return True # Don't actually need to return a form - - if (not getattr(view, 'get_serializer', None) or - not parsers.FormParser in getattr(view, 'parser_classes')): - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) - - ##### - # TODO: This is a little bit of a hack. Actually we'd like to remove - # this and just render serializer fields to html directly. - - # We need to map our Fields to Django's Fields. + def serializer_to_form_fields(self, serializer): field_mapping = { serializers.FloatField: forms.FloatField, serializers.IntegerField: forms.IntegerField, @@ -261,34 +313,72 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.DateField: forms.DateField, serializers.EmailField: forms.EmailField, serializers.CharField: forms.CharField, + serializers.ChoiceField: forms.ChoiceField, serializers.BooleanField: forms.BooleanField, - serializers.PrimaryKeyRelatedField: forms.ModelChoiceField, - serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField + serializers.PrimaryKeyRelatedField: forms.ChoiceField, + serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField, + serializers.SlugRelatedField: forms.ChoiceField, + serializers.ManySlugRelatedField: forms.MultipleChoiceField, + serializers.HyperlinkedRelatedField: forms.ChoiceField, + serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField } - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - - serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): - if getattr(v, 'readonly', True): + if getattr(v, 'read_only', True): continue kwargs = {} - if getattr(v, 'queryset', None): - kwargs['queryset'] = getattr(v, 'queryset', None) + kwargs['required'] = v.required + + #if getattr(v, 'queryset', None): + # kwargs['queryset'] = v.queryset + + if getattr(v, 'choices', None) is not None: + kwargs['choices'] = v.choices + + if getattr(v, 'widget', None): + widget = copy.deepcopy(v.widget) + kwargs['widget'] = widget + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + + kwargs['label'] = k try: fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: - fields[k] = forms.CharField() + if getattr(v, 'choices', None) is not None: + fields[k] = forms.ChoiceField(**kwargs) + else: + fields[k] = forms.CharField(**kwargs) + return fields + + def get_form(self, view, method, request): + """ + Get a form, possibly bound to either the input or output data. + In the absence on of the Resource having an associated form then + provide a form that can be used to submit arbitrary content. + """ + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): + return + + if method == 'DELETE' or method == 'OPTIONS': + return True # Don't actually need to return a form + + if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: + media_types = [parser.media_type for parser in view.parser_classes] + return self.get_generic_content_form(media_types) + + serializer = view.get_serializer(instance=obj) + fields = self.serializer_to_form_fields(serializer) + # Creating an on the fly form see: + # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted - data = serializer.data + data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance |
