Fork me on GitHub

The Renderer

An action comes in two parts: one part does the processing, and the other (known as the renderer) returns a response to the client. This allows for transparent content negotiation, and means you never have to write a separate ‘API’ for your site, or call render_to_response() at the bottom of every view function.

Renderer Backends

When an action is triggered by a request, the main body of the action is first run. If this does not return a HttpResponse outright, the renderer kicks in and performs content negotiation, to decide which renderer backend to use. Each backend is associated with a mimetype, so the renderer will examine the client headers and resolve a series of acceptable backends, which it will call in decreasing order of preference until one produces a response.

There are two types of renderer backend. The most common is the specific renderer backend, which is attached to a single action for a particular mimetype. Here’s a simple example of a backend for rendering a JSON representation of a user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dagny import Resource, action
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
import simplejson

class User(Resource):

    # ... snip! ...

    @action
    def show(self, username):
        self.user = get_object_or_404(User, username=username)

    @show.render.json
    def show(self):
        return HttpResponse(content=simplejson.dumps(self.user.to_dict()),
                            mimetype='application/json')

The decorator API is inspired by Python’s built-in property. As you can see, specific renderer backends are methods which accept only self (which will be the resource instance). They’re typically highly coupled with the resource and action they’re defined on; this one assumes the presence of self.user, for example.

Content Negotiation

Assume that the User resource is mounted at /users/. Now, if you fetch /users/zacharyvoase/, you’ll see the "users/show.html" template rendered as a HTML page. If you fetch /users/zacharyvoase/?format=json, however, you’ll get a JSON representation of that user.

Dagny’s ConNeg mechanism is quite sophisticated; webob.acceptparse is used to parse HTTP Accept headers, and these are considered alongside explicit format parameters. So, you could also have passed an Accept: application/json HTTP header in that last example, and it would have worked. If you’re using curl, you could try the following command:

curl -H"Accept: application/json" 'http://mysite.com/users/zacharyvoase/'

Skipping Renderers

Sometimes, you will define multiple renderer backends for an action, but in a few cases a single backend won’t be able to generate a response for that particular request. You can indicate this by raising dagny.renderer.Skip:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from dagny.renderer import Skip
from django.http import HttpResponse

class User(Resource):
    # ... snip! ...

    @show.render.rdf_xml
    def show(self):
        if not hasattr(self, 'graph'):
            raise Skip
        return HttpResponse(content=self.graph.serialize(),
                            content_type='application/rdf+xml')

The renderer will literally skip over this backend and on to the next-best preferred one. This feature really comes in handy when writing generic backends, which will only be able to determine at runtime whether they are suitable for a given action and request.

Additional MIME types

Additional renderers for a single action are defined using the decoration syntax (@<action_name>.render.<format>) as seen above, but since content negotiation is based on mimetypes, Dagny keeps a global dict (dagny.conneg.MIMETYPES) mapping these shortcodes to full mimetype strings. You can create your own shortcodes, and use them in resource definitions:

1
2
3
4
5
from dagny.conneg import MIMETYPES

MIMETYPES['rss'] = 'application/rss+xml'
MIMETYPES['png'] = 'image/png'
MIMETYPES.setdefault('json', 'text/javascript')

There is already a relatively extensive list of types defined; see the dagny.conneg module for more information.

Generic Backends

Dagny also supports generic renderer backends; these are backends attached to a Renderer instance which will be available on all actions by default. They are simple functions which take both the action instance and the resource instance. For example, the HTML renderer (which every action has as standard) looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from dagny.action import Action
from dagny.utils import camel_to_underscore, resource_name

from django.shortcuts import render_to_response
from django.template import RequestContext

@Action.RENDERER.html
def render_html(action, resource):
    template_path_prefix = getattr(resource, 'template_path_prefix', "")
    resource_label = camel_to_underscore(resource_name(resource))
    template_name = "%s%s/%s.html" % (template_path_prefix, resource_label, action.name)

    return render_to_response(template_name, {
      'self': resource
    }, context_instance=RequestContext(resource.request))

To go deeper, Action.RENDERER is a globally-shared instance of dagny.renderer.Renderer, whereas the render attribute on actions is actually a BoundRenderer. This split is what allows you to define specific backends that just take self (the resource instance), and generic backends which also take the action.

Each BoundRenderer has a copy of the whole set of generic backends, so you can operate on them as if they had been defined on that action:

class User(Resource):
    @action
    def show(self, username):
        self.user = get_object_or_404(User, username=username)

    # Remove the generic HTML backend from the `show` action alone.
    del show.render['html']

    # Item assignment, even on a `BoundRenderer`, takes generic backend
    # functions (i.e. functions which accept both the action *and* the
    # resource).
    show.render['html'] = my_generic_html_backend

Skipping in Generic Backends

As mentioned previously, dagny.renderer.Skip becomes very useful when writing generic backends. For example, here’s a backend which produces RDF/XML responses, but only if self.graph exists and is an instance of rdflib.Graph:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from dagny.action import Action
from dagny.conneg import MIMETYPES
from dagny.renderer import Skip
from django.http import HttpResponse
import rdflib

# This is already defined in Dagny by default.
MIMETYPES['rdf_xml'] = 'application/rdf+xml'

@Action.RENDERER.rdf_xml
def render_rdf_xml(action, resource):
    graph = getattr(resource, 'graph', None)
    if not isinstance(graph, rdflib.Graph):
        raise Skip

    return HttpResponse(content=graph.serialize(format='xml'),
                        content_type='application/rdf+xml')