Greg Aker

How do Django class-based views work?

Filed in: Django

April 19, 2012

Django version 1.3 released new generic class-based views. While these are wickedly cool, some of the documentation is a bit hard to see exactly what you can do with them. How about some simple use cases and dive into the code so we can learn how these work.

Let's start by reviewing what is in the documentation. We'll replace django.views.generic.simple.direct_to_template with django.views.generic.TemplateView.

We can directly replace it in urls.py like so:

from django.conf.urls.defaults import patterns, url
from django.views import generic

urlpatterns = patterns('',
    url(r'^about$', generic.TemplateView.as_view(
        template_name='about.html'),
        name='about-page'
    )
)

So let's trace this through the views stack and see what is happening. First off, we'll start in the TemplateView class. This view is subclassing TemplateResponseMixin, ContextMixin and View. Notice, it only defines one method called get().

Since we are calling a class method as_view() and passing some keyword arguments to it in urls.py, let's start by looking to see where that comes from. That brings us to the View class.

as_view() is simple enough. It cycles through the keyword arguments we send to it in urls.py. If you try to pass a keyword argument like foobar which doesn't exist in your view class, an exception will be raised. Next, in the view function, it will set self.head to self.get if you don't have a defined head method.

Next, it hits the dispatch() method and away we go. dispatch() tries to map to the right http method in your class. For instance, if it's a POST request, it will hit a post() method, get() for a GET request, etc. Make sense?

At this point, let's go back to TemplateView. We can see that TemplateView only adds a get() method. See how the View class is properly mapping the GET request to that get() method? Since head is mapped to get as well, anything other than those will cause an exception. In that get() method, it sets up your template context by calling self.get_context_data(). That sends us to the ContextMixin, which is super simple. It returns the keyword arguments sent to it. So if nothing is passed, then the context is an empty dictionary.

Next, it returns self.render_to_response() and passes the context it got before. render_to_response() comes from the TemplateResponseMixin. Basically, it returns a template response from django.template.response.TemplateResponse. While doing that it checks to see if you have properly configured a template_name keyword argument (like we did in urls.py), or setup a method called get_template_name(). Since we're calling it from urls.py we can't use get_template_name(). We must pass template_name to as_view().

Extra Context

But wait, what if you added extra context in urls.py with the function-based view and we need to mimic that in the new class-based view. This time, we'll jump into views.py and create a new class.

from django.views import generic

class AboutView(generic.TemplateView):
    """ About page view. """
    template_name = 'about.html'

    def get_context_data(self, **kwargs):
        ctx = super(AboutView, self).get_context_data(**kwargs)
        ctx['something_else'] = None  # add something to ctx
        return ctx

Just override get_context_data(). Remember where that's being called? ContextMixin right? Remember how we get to that? The get() method in generic.TemplateView. See not a ton of magic, you just need to trace!

Replacing the login_required decorator

It's in the docs, but you might have missed it. The dispatch method is where the get/post/head/etc methods are setup, and to make matters better, request is an argument passed to it here. So just override that and check to see if the user is logged in, and redirect or throw a 404 if they aren't.

from django import http
from django.views import generic

class AboutView(generic.TemplateView):
    """ About page view. """
    template_name = 'about.html'

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            raise http.Http404
        return super(AboutView, self).dispatch(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super(AboutView, self).get_context_data(**kwargs)
        ctx['something_else'] = None  # add something to ctx
        return ctx

So boom, the view is now protected from users who are not authenticated to the system.

We could possibly take this one step further and create an is authenticated mixin that we can reuse across different views. That would look something like this:

from django import http
from django.views import generic

class LoggedInMixin(object):
    """ A mixin requiring a user to be logged in. """
    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated():
            raise http.Http404
        return super(LoggedInMixin, self).dispatch(request, *args, **kwargs)

class AboutView(LoggedInMixin, generic.TemplateView):
    """ About page view. """
    template_name = 'about.html'

    def get_context_data(self, **kwargs):
        ctx = super(AboutView, self).get_context_data(**kwargs)
        ctx['something_else'] = None  # add something to ctx
        return ctx

See how handy this can be?

Spend some time with class-based views. They can really make for wicked clean code. It's a bit hard to see where some things are coming from, but as you get more comfortable with how they work, the clean-code tradeoff is really worth it.

Shameless plug

Want to learn some more Python? Check out my screencasts at Mijingo.