diff options
| author | Tom Christie | 2011-04-29 14:32:56 +0100 | 
|---|---|---|
| committer | Tom Christie | 2011-04-29 14:32:56 +0100 | 
| commit | b358fbdbe9cbd4ce644c4b2c7b9b4cec0811e14e (patch) | |
| tree | 601e5576995809b74f914cafcee8a7a8cd9c6937 /djangorestframework/renderers.py | |
| parent | 93aa065fa92f64472a3ee80564020a81776be742 (diff) | |
| download | django-rest-framework-b358fbdbe9cbd4ce644c4b2c7b9b4cec0811e14e.tar.bz2 | |
More refactoring - move various less core stuff into utils etc
Diffstat (limited to 'djangorestframework/renderers.py')
| -rw-r--r-- | djangorestframework/renderers.py | 243 | 
1 files changed, 243 insertions, 0 deletions
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py new file mode 100644 index 00000000..e53dc061 --- /dev/null +++ b/djangorestframework/renderers.py @@ -0,0 +1,243 @@ +"""Renderers are used to serialize a Resource'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 Resource, output status and headers, +and providing forms and links depending on the allowed methods, renderers and parsers on the Resource.  +""" +from django import forms +from django.conf import settings +from django.template import RequestContext, loader +from django.utils import simplejson as json +from django import forms + +from djangorestframework.utils import dict2xml, url_resolves +from djangorestframework.compat import apply_markdown +from djangorestframework.utils.breadcrumbs import get_breadcrumbs +from djangorestframework.utils.description import get_name, get_description +from djangorestframework import status + +from urllib import quote_plus +import string +import re +from decimal import Decimal + +# TODO: Rename verbose to something more appropriate +# TODO: Maybe None could be handled more cleanly.  It'd be nice if it was handled by default, +#       and only have an renderer output anything if it explicitly provides support for that. + +class BaseRenderer(object): +    """All renderers must extend this class, set the media_type attribute, and +    override the render() function.""" +    media_type = None + +    def __init__(self, resource): +        self.resource = resource + +    def render(self, output=None, verbose=False): +        """By default render simply returns the ouput as-is. +        Override this method to provide for other behaviour.""" +        if output is None: +            return '' +         +        return output + + +class TemplateRenderer(BaseRenderer): +    """Provided for convienience. +    Emit the output by simply rendering it with the given template.""" +    media_type = None +    template = None + +    def render(self, output=None, verbose=False): +        if output is None: +            return '' + +        context = RequestContext(self.request, output) +        return self.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, resource, request, output): +        """Get the content as if it had been renderted 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 resource.renderers if not isinstance(renderer, DocumentingTemplateRenderer)] +        if not renderers: +            return '[No renderers were found]' +         +        content = renderers[0](resource).render(output, verbose=True) +        if not all(char in string.printable for char in content): +            return '[%d bytes of binary content]' +             +        return content +             + +    def _get_form_instance(self, resource): +        """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.""" +        # Get the form instance if we have one bound to the input +        #form_instance = resource.form_instance +        # TODO! Reinstate this + +        form_instance = getattr(resource, 'bound_form_instance', None) + +        if not form_instance and hasattr(resource, 'get_bound_form'): +            # Otherwise if we have a response that is valid against the form then use that +            if resource.response.has_content_body: +                try: +                    form_instance = resource.get_bound_form(resource.response.cleaned_content) +                    if form_instance and not form_instance.is_valid(): +                        form_instance = None +                except: +                    form_instance = None +             +        # If we still don't have a form instance then try to get an unbound form +        if not form_instance: +            try: +                form_instance = resource.get_bound_form() +            except: +                pass + +        # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types +        if not form_instance: +            form_instance = self._get_generic_content_form(resource) +         +        return form_instance + + +    def _get_generic_content_form(self, resource): +        """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 resource won't treat the form's value as the content of the request. +        if not getattr(resource, 'USE_FORM_OVERLOADING', False): +            return None + +        # NB. http://jacobian.org/writing/dynamic-form-generation/ +        class GenericContentForm(forms.Form): +            def __init__(self, resource): +                """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 resource.parsed_media_types] +                initial_contenttype = resource.default_parser.media_type + +                self.fields[resource.CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', +                                                                            choices=contenttype_choices, +                                                                            initial=initial_contenttype) +                self.fields[resource.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.resource.CONTENTTYPE_PARAM is None or self.resource.CONTENT_PARAM is None: +            return None + +        # Okey doke, let's do it +        return GenericContentForm(resource) + + +    def render(self, output=None): +        content = self._get_content(self.resource, self.resource.request, output) +        form_instance = self._get_form_instance(self.resource) + +        if url_resolves(settings.LOGIN_URL) and url_resolves(settings.LOGOUT_URL): +            login_url = "%s?next=%s" % (settings.LOGIN_URL, quote_plus(self.resource.request.path)) +            logout_url = "%s?next=%s" % (settings.LOGOUT_URL, quote_plus(self.resource.request.path)) +        else: +            login_url = None +            logout_url = None + +        name = get_name(self.resource) +        description = get_description(self.resource) + +        markeddown = None +        if apply_markdown: +            try: +                markeddown = apply_markdown(description) +            except AttributeError:  # TODO: possibly split the get_description / get_name into a mixin class +                markeddown = None + +        breadcrumb_list = get_breadcrumbs(self.resource.request.path) + +        template = loader.get_template(self.template) +        context = RequestContext(self.resource.request, { +            'content': content, +            'resource': self.resource, +            'request': self.resource.request, +            'response': self.resource.response, +            'description': description, +            'name': name, +            'markeddown': markeddown, +            'breadcrumblist': breadcrumb_list, +            'form': form_instance, +            'login_url': login_url, +            'logout_url': logout_url, +            'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX +        }) +         +        ret = template.render(context) + +        return ret + + +class JSONRenderer(BaseRenderer): +    """Renderer which serializes to JSON""" +    media_type = 'application/json' + +    def render(self, output=None, verbose=False): +        if output is None: +            return '' +        if verbose: +            return json.dumps(output, indent=4, sort_keys=True) +        return json.dumps(output) + + +class XMLRenderer(BaseRenderer): +    """Renderer which serializes to XML.""" +    media_type = 'application/xml' + +    def render(self, output=None, verbose=False): +        if output is None: +            return '' +        return dict2xml(output) + + +class DocumentingHTMLRenderer(DocumentingTemplateRenderer): +    """Renderer which provides a browsable HTML interface for an API. +    See the examples listed in the django-rest-framework documentation to see this in actions.""" +    media_type = 'text/html' +    template = 'renderer.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' +    template = 'renderer.html' + + +class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): +    """Renderer that serializes the output with the default renderer, but also provides plain-text +    doumentation 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' +    template = 'renderer.txt' +     +DEFAULT_RENDERERS = ( JSONRenderer, +                     DocumentingHTMLRenderer, +                     DocumentingXHTMLRenderer, +                     DocumentingPlainTextRenderer, +                     XMLRenderer ) + +  | 
