Django Patterns: View Decorators

Problem and Analysis

Sites often have many views that operate with a similar set of assumptions.
Maybe there are entire areas that the user must be logged-in to visit, or there
is some repetitive boilerplate functionality that a group of views shares like
being rate-limited. Bolting on “login required” or “rate limiting” functionality
can be a bit repetitive since it often requires “bail out early” logic. Take
for example a simple rate-limiting implementation:

syntax:python
def simple_rate_limiting(request, duration=10):
    # grab the end-user's IP address
    remote_addr = request.META.get('REMOTE_ADDR')

    # create a cache key combining IP and url requested
    key = '%s.%s' % (remote_addr, request.get_full_path())

    # if the key exists then the user has been here recently
    if cache.get(key):
        return True
    else:
        cache.set(key, 1, duration)
    return False

def rate_limited_view(request):
    if simple_rate_limiting(request):
        return HttpResponseForbidden('Slow down!')

    # normal view logic continues here #

There is nothing particularly bad about this implementation – we are checking
for a condition based on the request and if necessary bailing out early.
The problem starts to appear when additional “common” components get added to
the mix, such as:

  • loading an object based on a url param
  • checking for auth
  • adding something to the template context/response

And now for a contrived example. Consider the following urls:

syntax:python
from django.conf.urls.defaults import *

urlpatterns = patterns('groups.views',
    url(r'^(?P<group_slug>[\w-]+)/$', 'group_detail', name='group_detail'),
    url(r'^(?P<group_slug>[\w-]+)/edit/$', 'group_edit', name='group_edit'),
    url(r'^(?P<group_slug>[\w-]+)/members/$', 'member_list', name='group_member_list'),
)

On all of these views we need to:

  1. get the group object specified by the slug or return a 404
  2. check that the requesting user is a member of the group or return a 403
  3. add the group to the template context so its always available

All of this code is required to do even the most basic view, the “object detail” view.

syntax:python
def group_detail(request, group_slug, template_name='group_detail.html'):
    # get our group or raise a 404 -> so far so good
    group = get_object_or_404(Group, slug=group_slug)

    # because we're bailing out early, this one will be two lines
    if not group.members.filter(pk=request.user.pk).exists():
        return HttpResponseForbidden()

    # make sure our group is in the context
    return render_to_response(template_name, {'group': group},
        context_instance=RequestContext(request))

This could get very repetitive!

Solution

Django views are callables that take a HttpRequest object and any number of
other parameters and emit an HttpResponse object. Because of these simple
guarantees, which specify to some degree the input and output formats, it is
a good match for using Decorators
which replace the wrapped callable with a new function optionally altering
behavior before or after the execution of the wrapped callable.

Most Django developers are familiar with the login_required
decorator, which before handing control to the view itself, checks to see if
the user is logged in and if not returns a HttpResponseRedirect (302) to the
login page. This basic idea can be used to do any number of interesting things,
for example:

Returning to the groups example above, all three tasks identified can be handled by a decorator so let’s see the first one, getting a group or raising a 404:

syntax:python
from django.utils.functional import wraps # keeps name/docstring/etc intact

def group_view_wrapper(view):
    @wraps(view)
    def inner(request, group_slug, *args, **kwargs):
        group = get_object_or_404(Group, slug=group_slug)

        # notice that we're calling the wrapped view but instead of passing
        # in the group_slug parameter, we're passing in the actual group object
        return view(request, group, *args, **kwargs)

    # return the wrapped function, replacing the original view
    return inner

@group_view
def group_detail(request, group, template_name='group_detail.html'):
    # now instead of group_slug our view receives an actual group!
    # still need to check if the user is a member of the group 
    # before continuing

To make it more clear what is actually going on, the following produces the same
result. In effect we are replacing our original function with a wrapped copy
of itself that performs additional logic before actually executing the original:

syntax:python
def group_detail(request, group, template_name='group_detail.html'):
    # now instead of group_slug our view receives an actual group!
    ... do whatever ...

group_detail = group_view_wrapper(group_detail)

Now, to take care of the second part of the problem – short circuiting the view
logic if the user is not a member of the group. This takes place before the
view is even processed, so like the object lookup, perform the check before
actually calling the original view.

syntax:python
def group_view_wrapper(view):
    @wraps(view)
    def inner(request, group_slug, *args, **kwargs):
        group = get_object_or_404(Group, slug=group_slug)

        # check for membership, returning a 403 if not a member
        if not group.members.filter(pk=request.user.pk).exists():
            # this short circuits - the view is never even called!
            return HttpResponseForbidden()

        return view(request, group, *args, **kwargs)
    return inner

@group_view
def group_detail(request, group, template_name='group_detail.html'):
    # now all that is left is to write our view and select the right template

The final part of the decorator will make sure that the group gets added to the
template context. Up to now the decorator has only altered things going into
the view — now the decorator will alter data on the way out.

syntax:python
def group_view_wrapper(view):
    @wraps(view)
    def inner(request, group_slug, *args, **kwargs):
        group = get_object_or_404(Group, slug=group_slug)

        # check for membership, returning a 403 if not a member
        if not group.members.filter(pk=request.user.pk).exists():
            # this short circuits - the view is never even called!
            return HttpResponseForbidden()

        # our view will now return a template name and a dictionary of
        # context instead of an HttpResponse
        template_name, dictionary = view(request, group, *args, **kwargs)

        # update the context dictionary with the group
        dictionary.update(group=group)

        # render the template and context to a response and return it
        return render_to_response(template_name, dictionary,
            context_instance=RequestContext(request))

    return inner

@group_view
def group_detail(request, group, template_name='group_detail.html'):
    # whoa, no more boilerplate code
    return template_name, {}

You will still need to decorate all of your views but that one line is more
manageable than all that copying/pasting.

Strengths and Weaknesses

The clear winner here is DRY – your code is cleaner by using decorators.

The biggest weakness I see is that you may be breaking expectations, which can
make reading your code a pain in the ass for others. In the example above, we are
converting the group_slug into a group – if you looked only at the url pattern
and the view declaration it would not be immediately apparent that inside the
decorator the group_slug is actually converted into a group object.

Furthermore, when you use decorators for things like wrapping render_to_response,
you “appear” to break the contract of a view receiving a request and returning a
response:

syntax:python
@render_response
def homepage(request):
    latest_activity = Activity.objects.latest()
    return 'homepage.html', {'activity_list': latest_activity}

Here you can see we’re returning a tuple of template_name and dictionary.
It is not clear to someone unfamiliar with the code that this actually is
equivalent to:

syntax:python
def homepage(request):
    latest_activity = Activity.objects.latest()
    return render_to_response('homepage.html', {'activity_list': latest_activity},
        context_instance=RequestContext(request))

Possible ghettohax

Wrapping url()

One hack I’ve used in the past when handling things like this is to actually
write a wrapper around the url function.
This eliminates some of the cruft you get when you have 20+ url patterns that
all begin with “group_slug”.

Here’s what I mean:

syntax:python
from django.conf.urls.defaults import *
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404

from groups import views

def group_view_wrapper(view):
    """
    Same decorator as earlier
    """
    @wraps(view)
    def inner(request, group_slug, *args, **kwargs):
        ...
    return inner

def group_url(regex, view, kwargs=None, name=None):
    """
    This wraps the default url() function, prepending our <group_slug> bits
    to the regex and decorating the view all at once
    """
    if regex.startswith('^'): regex = regex[1:] # peel off the leading caret

    regex = r'^(?P<group_slug>[-\w]+)/' + regex

    # notice that we wrap the view with our group_view_wrapper decorator
    return url(regex, group_view_wrapper(view), kwargs, name)

# this is just a generic group list view - it doesn't take a group_slug
urlpatterns = patterns('groups.views',
    url(r'^$', 'group_list', name='group_list'),
)

# notice both that we're referencing the actual view in the second arg as
# opposed to the quoted name of the view, and also using our own group_url
# function as opposed to the usual url()
group_urlpatterns = patterns('',
    group_url(r'^$', views.group_detail, name='group_detail'),
    group_url(r'^edit/$', views.group_edit, name='group_edit'),
    group_url(r'^members/$', views.group_members, name='group_members'),
)

urlpatterns += group_urlpatterns

Dispatching to generic views

Generic views generally travel in a pack (year/month/day/detail), and there is
often a lot of common ground between those 4 views. In the past I’ve just
pointed at a “dispatch” type function that sends the request to the correct
date-based view:

  1. All 4 url patterns point to the same view function, entry_archive (shown below)
  2. All 4 url patterns are different but use a standard naming: year/month/day/slug
  3. All 4 url patterns have different names so they can be reversed individually

From the urls.py:

syntax:python
urlpatterns = patterns('awesomeblog.views',
    url(r'^(?P<year>\d+)/$', 
        'entry_archive',
        name='entry_archive_year'
    ),
    url(r'^(?P<year>\d+)/(?P<month>\w+)/$', 
        'entry_archive',
        name='entry_archive_month'
    ),
    url(r'^(?P<year>\d+)/(?P<month>\w+)/(?P<day>\d+)/$',
        'entry_archive',
        name='entry_archive_day'
    ),
    url(r'^(?P<year>\d+)/(?P<month>\w+)/(?P<day>\d+)/(?P<slug>[-\w]+)/$', 
        'entry_archive', 
        name='entry_detail'
    ),
)

In the entry_archive view, its now a matter of grabbing the common context and
applying it to the best generic view:

syntax:python
def entry_archive(request, year, month=None, day=None, slug=None):
    common_context = {
        'request': request,
        'queryset': Entry.objects.published(),
        'date_field': 'pub_date',
        'extra_context': {'tags': Tag.objects.all(), 'foo': 'bar'},
    }

    if slug:
        return date_based.object_detail(
            year=year,
            month=month,
            day=day,
            slug=slug,
            **common_context
        )
    if day:
        return date_based.archive_day(
            year=year,
            month=month,
            day=day,
            **common_context
        )
    if month:
        return date_based.archive_month(
            year=year,
            month=month,
            **common_context
        )
    if year:
        return date_based.archive_year(
            year=year,
            **common_context
        )

Conclusion

I hope you found this information useful! Wrapping views is convenient way to
make your code more DRY and fits well with the “give me a request, I’ll give you
a response” workflow (even if it does produce code that may not be immediately
obvious to those unfamiliar with the codebase). I’m planning a couple more
entries in this vein, “Django Patterns”, so keep an eye out for new posts! As
always, any comments, feedback, suggestions, errata, etc are appreciated. Thanks
for reading.

Read full article at “charlesleifer.com: Entries tagged with "django"”

Leave a comment