diff options
Diffstat (limited to 'rest_framework/renderers.py')
| -rw-r--r-- | rest_framework/renderers.py | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py new file mode 100644 index 00000000..66394af3 --- /dev/null +++ b/rest_framework/renderers.py @@ -0,0 +1,402 @@ +""" +Renderers are used to serialize a View's output 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. +""" +from django import forms +from django.template import RequestContext, loader +from django.utils import simplejson as json + +from rest_framework.compat import yaml +from rest_framework.settings import api_settings +from rest_framework.utils import dict2xml +from rest_framework.utils import encoders +from rest_framework.utils.breadcrumbs import get_breadcrumbs +from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches +from rest_framework import VERSION +from rest_framework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField + +import string + + +__all__ = ( + 'BaseRenderer', + 'TemplateRenderer', + 'JSONRenderer', + 'JSONPRenderer', + 'DocumentingHTMLRenderer', + 'DocumentingXHTMLRenderer', + 'DocumentingPlainTextRenderer', + 'XMLRenderer', + 'YAMLRenderer' +) + + +class BaseRenderer(object): + """ + All renderers must extend this class, set the :attr:`media_type` attribute, + and override the :meth:`render` method. + """ + + _FORMAT_QUERY_PARAM = 'format' + + media_type = None + format = None + + def __init__(self, view=None): + self.view = view + + def can_handle_format(self, format): + return format == self.format + + def can_handle_media_type(self, media_type): + """ + Returns `True` if this renderer is able to deal with the given + media type. + + The default implementation for this function is to check the media type + argument against the media_type attribute set on the class to see if + they match. + + This may be overridden to provide for other behavior, but typically + you'll instead want to just set the `media_type` attribute on the class. + """ + return media_type_matches(self.media_type, media_type) + + def render(self, obj=None, media_type=None): + """ + Given an object render it into a string. + + The requested media type is also passed to this method, + as it may contain parameters relevant to how the parser + should render the output. + EG: ``application/json; indent=4`` + + By default render simply returns the output as-is. + Override this method to provide for other behavior. + """ + if obj is None: + return '' + + return str(obj) + + +class JSONRenderer(BaseRenderer): + """ + Renderer which serializes to JSON + """ + + media_type = 'application/json' + format = 'json' + encoder_class = encoders.JSONEncoder + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized JSON. + """ + if obj is None: + return '' + + # If the media type looks like 'application/json; indent=4', then + # pretty print the result. + indent = get_media_type_params(media_type).get('indent', None) + sort_keys = False + try: + indent = max(min(int(indent), 8), 0) + sort_keys = True + except (ValueError, TypeError): + indent = None + + return json.dumps(obj, cls=self.encoder_class, indent=indent, sort_keys=sort_keys) + + +class JSONPRenderer(JSONRenderer): + """ + Renderer which serializes to JSONP + """ + + media_type = 'application/javascript' + format = 'jsonp' + renderer_class = JSONRenderer + callback_parameter = 'callback' + + def _get_callback(self): + return self.view.request.GET.get(self.callback_parameter, self.callback_parameter) + + def _get_renderer(self): + return self.renderer_class(self.view) + + def render(self, obj=None, media_type=None): + callback = self._get_callback() + json = self._get_renderer().render(obj, media_type) + return "%s(%s);" % (callback, json) + + +class XMLRenderer(BaseRenderer): + """ + Renderer which serializes to XML. + """ + + media_type = 'application/xml' + format = 'xml' + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized XML. + """ + if obj is None: + return '' + return dict2xml(obj) + + +class YAMLRenderer(BaseRenderer): + """ + Renderer which serializes to YAML. + """ + + media_type = 'application/yaml' + format = 'yaml' + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized YAML. + """ + if obj is None: + return '' + + return yaml.safe_dump(obj) + + +class TemplateRenderer(BaseRenderer): + """ + A Base class provided for convenience. + + 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. + """ + + media_type = None + template = None + + def render(self, obj=None, media_type=None): + """ + Renders *obj* using the :attr:`template` specified on the class. + """ + if obj is None: + return '' + + template = loader.get_template(self.template) + context = RequestContext(self.view.request, {'object': obj}) + return template.render(context) + + +class DocumentingTemplateRenderer(BaseRenderer): + """ + Base class for renderers used to self-document the API. + Implementing classes should extend this class and set the template attribute. + """ + + template = None + + def _get_content(self, view, request, obj, media_type): + """ + Get the content as if it had been rendered by a non-documenting renderer. + + (Typically this will be the content as it would have been if the Resource had been + requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.) + """ + + # Find the first valid renderer and render the content. (Don't use another documenting renderer.) + renderers = [renderer for renderer in view.renderer_classes + if not issubclass(renderer, DocumentingTemplateRenderer)] + if not renderers: + return '[No renderers were found]' + + media_type = add_media_type_param(media_type, 'indent', '4') + content = renderers[0](view).render(obj, media_type) + if not all(char in string.printable for char in content): + return '[%d bytes of binary content]' + + return content + + def _get_form_instance(self, view, method): + """ + 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. + """ + if not hasattr(self.view, 'get_serializer'): # No serializer, no form. + return + # We need to map our Fields to Django's Fields. + field_mapping = dict([ + [FloatField.__name__, forms.FloatField], + [IntegerField.__name__, forms.IntegerField], + [DateTimeField.__name__, forms.DateTimeField], + [DateField.__name__, forms.DateField], + [EmailField.__name__, forms.EmailField], + [CharField.__name__, forms.CharField], + [BooleanField.__name__, forms.BooleanField] + ]) + + # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python + fields = {} + object, data = None, None + if hasattr(self.view, 'object'): + object = self.view.object + serializer = self.view.get_serializer(instance=object) + for k, v in serializer.fields.items(): + fields[k] = field_mapping[v.__class__.__name__]() + OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) + if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + data = serializer.data + form_instance = OnTheFlyForm(data) + return form_instance + + def _get_generic_content_form(self, view): + """ + Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms + (Which are typically application/x-www-form-urlencoded) + """ + + # 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 value as the content of the request. + if not getattr(view.request, '_USE_FORM_OVERLOADING', False): + return None + + # NB. http://jacobian.org/writing/dynamic-form-generation/ + class GenericContentForm(forms.Form): + def __init__(self, view, request): + """We don't know the names of the fields we want to set until the point the form is instantiated, + as they are determined by the Resource the form is being created against. + Add the fields dynamically.""" + super(GenericContentForm, self).__init__() + + contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types] + initial_contenttype = view._default_parser.media_type + + self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', + choices=contenttype_choices, + initial=initial_contenttype) + self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content', + widget=forms.Textarea) + + # If either of these reserved parameters are turned off then content tunneling is not possible + if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None: + return None + + # Okey doke, let's do it + return GenericContentForm(view, view.request) + + def get_name(self): + try: + return self.view.get_name() + except AttributeError: + return self.view.__doc__ + + def get_description(self, html=None): + if html is None: + html = bool('html' in self.format) + try: + return self.view.get_description(html) + except AttributeError: + return self.view.__doc__ + + def render(self, obj=None, media_type=None): + """ + Renders *obj* using the :attr:`template` set on the class. + + The context used in the template contains all the information + needed to self-document the response to this request. + """ + + content = self._get_content(self.view, self.view.request, obj, media_type) + + put_form_instance = self._get_form_instance(self.view, 'put') + post_form_instance = self._get_form_instance(self.view, 'post') + + name = self.get_name() + description = self.get_description() + + breadcrumb_list = get_breadcrumbs(self.view.request.path) + + template = loader.get_template(self.template) + context = RequestContext(self.view.request, { + 'content': content, + 'view': self.view, + 'request': self.view.request, + 'response': self.view.response, + 'description': description, + 'name': name, + 'version': VERSION, + 'breadcrumblist': breadcrumb_list, + 'allowed_methods': self.view.allowed_methods, + 'available_formats': self.view._rendered_formats, + 'put_form': put_form_instance, + 'post_form': post_form_instance, + 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, + 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), + 'api_settings': api_settings + }) + + ret = template.render(context) + + # Munge DELETE Response code to allow us to return content + # (Do this *after* we've rendered the template so that we include + # the normal deletion response code in the output) + if self.view.response.status_code == 204: + self.view.response.status_code = 200 + + return ret + + +class DocumentingHTMLRenderer(DocumentingTemplateRenderer): + """ + Renderer which provides a browsable HTML interface for an API. + See the examples at http://api.django-rest-framework.org to see this in action. + """ + + media_type = 'text/html' + format = 'html' + template = 'rest_framework/api.html' + + +class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): + """ + Identical to DocumentingHTMLRenderer, except with an xhtml media type. + We need this to be listed in preference to xml in order to return HTML to WebKit based browsers, + given their Accept headers. + """ + + media_type = 'application/xhtml+xml' + format = 'xhtml' + template = 'rest_framework/api.html' + + +class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): + """ + Renderer that serializes the object with the default renderer, but also provides plain-text + documentation of the returned status and headers, and of the resource's name and description. + Useful for browsing an API with command line tools. + """ + + media_type = 'text/plain' + format = 'txt' + template = 'rest_framework/api.txt' + + +DEFAULT_RENDERERS = ( + JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer +) + +if yaml: + DEFAULT_RENDERERS += (YAMLRenderer, ) +else: + YAMLRenderer = None |
