DRY in Django views

This post was written on 2008/04/19 at 08:52:20 by Horst Gutmann

A couple of days ago, a topic came up on the django-users mailinglist where someone asked how to avoid code repetition in views if the views mostly do the same (for instance do some processing on the request parameters or check a certain session variable etc.).

There is one question you have to ask yourself first, though: Does this common processing require request-specific data like session-data or request parameters?

In either case, you have multiple options. For instance you could write a function or decorator to do this common processing. This only makes sense, though, if most of the processing results end up in for instance the request object itself again. Otherwise you'd simply have way too many return values to keep the code maintainable.

Another option would be to convert your view into a class and put shared processing for instance into the constructor.

If you now want to for example handle a couple of request-parameters in the same way in multiple views, option 2 is actually pretty simple. You could, for instance, get something quite flexible while requiring only 3 components: (1) the actual view class, (2) a small factory function to convert such a class into a usable view and (3) the view function that comes out of the factory function

def create_view(klass):
    def _func(request, *args, **kwargs):
        after = '__after__'
        o = klass(request, *args, **kwargs)
        r = o(request, *args, **kwargs)
        if hasattr(o, after):
            return getattr(o, after)(r)
        return r
    return _func

class BaseView(object):
    def __init__(self, request, *args, **kwargs):
        # Do some generic stuff here
        pass
    def __call__(self, *args, **kwargs):
        raise RuntimeError, "Do not call the BaseView"
    def __after__(self, response):
        return response

class MyView(BaseView):
    def __call__(self, request, *args, **kwargs):
        return HttpResponse("Hello World")

myview = create_view(MyView)

With a structure like this you'd put all the functionality that is shared by your views into the __init__-method of the BaseView and then put the view-specific stuff into the __call__-method. And if you want to do some postprocessing, just overwrite the __after__-method.

If you want to use decorators like django.contrib.auth's login_required, you can do so by decorating the generated dview function:

myview = login_required(create_view(MyView))

If you don't do any request-specific processing that you want to aggregate into one function, you could also do something like this (similar to how the new django.contrib.formtools.wizard module does it):

#--- views.py ---

class MyView(object):
    def __init__(self, *args, **kwargs):
        # Do something
        pass

    def __call__(self, request, *args, **kwargs):
        # Actual view with response generation etc.
        return HttpResponse("Hello World")

#--- urls.py ---

from django.conf.urls.defaults import *
from .views import MyView

urlpatterns = patterns('',
    (r'^/?', MyView()),
)

Here you more or less only use the constructor to configure your view while in the previous approach you can also use it to handle request parameters.

In 2006 Rob Wolfe posted a similar approach without putting a class instance into the URL configuration, which has the advantage, that you don't have to actually import the view module upfront. Here the actual view object is created when the views.py is first imported, which -- as in the approach above -- does the whole common processing as rarely as possible.

Comments:

  • Hotsyk (Guest)

    Hi. You solution is pretty and fine. But I'd like to make some additions - e.g. insert more base classes and make some small changes in basic logic. Could I fork you solution, create my and put it into opensource? If so, which license you require?

    Thank you

    Sept. 28, 2008, 10:32 p.m.

  • zerok

    I'm currently in the process of moving this package out of django-zsutils and into its own standalone library since I plan to keep improving this.

    The code currently is and will stay under the BSD license so you're free to fork it if you want. But if you have something great to share, github keeps the attribution in order so a patch would be very welcome if you want to have something in there ;-) I hope I will have the whole move finished by tomorrow (and perhaps with some pypi registration by the day after tomorrow) :-)

    Sept. 29, 2008, 10:29 p.m.

  • Marc Fargas (Guest)

    The Class thing is already taking place with the Django built-in generic views: http://code.djangoproject.com/ticket/6735

    That should avoid having to wrap generic views, and allow further more customization ;)

    Note: If you want to check the code make sure you check the one in GIT (link in one of the comments) as the one attached is not yet up-to-date ;)

    Oct. 1, 2008, 11:12 p.m.

  • zerok

    I never said that my solution was completely new, but IMO it is different enough from the approach taken in the "new" generic views implementation.

    And contrary to what everyone out there seems to think: I am aware of that implementation ;-)

    Oct. 1, 2008, 11:18 p.m.

  • Myny (Guest)

    Hi, Thanks for this article. I am new to Django and was looking for something like this. I have one question. How can I redirect from the BaseClass? I want to handle login out in it, so I check if user clicked 'log out' button. Unless there is a other way to handle this?

    Nov. 25, 2008, 10:20 p.m.

*'d input fields are required.