Class-Based Generic Views in Django

I have a confession to make: Even though thoughtbot is mostly known for the work
we do with Ruby on Rails, I’m a huge Django fan. Someone left the keys to the
blog lying around, so I thought I’d take it for a quick joy ride around the one
of my favourite Django features: class-based generic views.

Firstly, just to help anyone who’s not au fait with Django terminology
catch up, a view deals with a request and does whatever needs to be
done to produce the correct response. If Rails is your thing, you can think of
it as being roughly equivalent to a controller action.

Class-based views were introduced in Django 1.3, but despite being around for a
couple of years they’re still not as widely used as they should be.

What came before

Before the class-based view there was the humble function-based view. It was a
simple time. Views would take a request and return a response. Here’s an
example for displaying a single blog post:

The mechanism is undoubtedly simple, but the resulting code isn’t. It’s very
dense, with a single function loading the context data and building the
response. This small example isn’t too bad, but imagine if we added comments,
and author information, and a list of related posts. We’d quickly get to
something very unwieldy.

Class-based views

It would be great if we could encapsulate all of this logic in a class so that
it was less dense and easier to extend. Conveniently Django 1.3 helps us to do
just that by providing a View class that we can extend. Here’s our blog post
detail view, refactored into a class:

The readability and extensibility have definitely improved, but the code could
still be better. There are a lot of small decisions encoded in the class that
needn’t be here. In writing this code I had to decide what to call the template,
what to call the URL keyword argument that contains the model’s primary key, and
what to call the context variable that is passed to template. None of those
decisions were hard to make, but none of them really matter that much. What
matters is that they remain consistent between different views, so that
developers don’t have to waste time looking for things only to discover that
this view doesn’t quite work like the last one they used.

In a nutshell, what this code needs is conventions. Application level
conventions are good, but framework level conventions are better: The same
developer can quickly understand many applications and easily move between
projects or even companies without needing as much time to get up to speed. As
consultants, when we’re working with new clients framework level conventions are
invaluable. Rails is famed for its opinionated stance on convention over
configuration, while Django is generally quieter on the subject. This might lead
you to believe that Django doesn’t provide any conventions, but there’s more to
Django than meets the eye.

Class-based generic views

Convention is where the “generic” part of “class-based generic views” comes into
play. Django provides subclasses of View for a variety of common
situations that are packed full of conventions and take all of those pesky
little decisions out of our hands.

Using the generic DetailView, which displays details of a single model
instance, we can boil the previous example down to a few simple lines:

Convention and configuration

Of course, convention stops being helpful as soon as you want to do something
unconventional. That’s where configuration comes into its own. Thankfully,
Django’s class-based generic views provide both by using the Template Method
pattern which makes it very easy to customise each part of the
generic process.

Let’s do something less conventional and update our BlogPostDetailView to only
display posts with a published flag set to True. We can do this by
providing the view with a QuerySet to use as the basis of its query:

The code is still very concise and readable. Template Method has allowed us to
override one part of the algorithm without reimplementing the whole thing. When
we return to this class in the future there aren’t dozens of lines of
boilerplate code to read through to find the significant parts.

Behind the scenes, the DetailView class provides an implementation of get,
which in turn calls the get_object method. get_object is the template
method, so if we wanted to buck all of the conventions we could override it.
Since we’re only concerned with changing the queryset, we can ignore the
template method itself and turn our attention to get_queryset, which is one of
the primitive operations that get_object uses. In the first example we take
advantage of the default implementation of get_queryset, which will return
self.queryset if we’ve provided it. In the second example we override
get_queryset entirely. In both cases we keep the rest of get_object’s
algorithm, including useful features like 404 Not Found responses and support
for looking up the model instance by primary key or using a slug.

Knowing what to override

The downside isn’t reading existing views, but writing new ones. The
documentation for this feature has improved significantly since Django 1.3 was
released, but there’s still something of a learning curve. Of course, you’re
going to end up learning a set of conventions for your application, so it may as
well be the one Django provides.

The good news is that these class-based generic views are very TDD friendly. If
you are missing some required configuration attribute Django will raise an
ImproperlyConfigured exception with a helpful message outlining your options
(usually either setting an attribute, or overriding one or more methods that
depend on the missing attribute). There’s rarely such a clear example of a
failing test telling you exactly what to do next.

There are also some very helpful resources out there to get you started and to
use as reference material when you’re up and running:

The official class-based views reference for specifics of
individual classes and methods. In particular, this includes a “method
flowchart” for each class which is helpful for figuring out what the
Template Methods and their primitive operations are called.