aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework/renderers.py
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework/renderers.py')
-rw-r--r--rest_framework/renderers.py188
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