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:
def blog_post_detail_view(request, *args, **kwargs):
"""Displays the details of a BlogPost"""
blog_post = get_object_or_404(BlogPost, pk=kwargs.get("pk"))
return TemplateResponse(request, "blog/blog_post.html", {
"blog_post": 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:
class BlogPostDetailView(View):
"""Displays the details of a BlogPost"""
def get(self, request, *args, **kwargs):
return TemplateResponse(request, self.get_template_name(),
self.get_context_data())
def get_template_name(self):
"""Returns the name of the template we should render"""
return "blog/blogpost_detail.html"
def get_context_data(self):
"""Returns the data passed to the template"""
return {
"blogpost": self.get_object(),
}
def get_object(self):
"""Returns the BlogPost instance that the view displays"""
return get_object_or_404(BlogPost, pk=self.kwargs.get("pk"))
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:
class BlogPostDetailView(DetailView):
"""Displays the details of a BlogPost"""
model = BlogPost
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:
class BlogPostDetailView(DetailView):
"""Displays the details of a BlogPost"""
model = BlogPost
queryset = BlogPost.objects.filter(published=True)
Alternatively, we can go one step further and override the get_queryset
method and use different querysets based on the properties of the request:
class BlogPostDetailView(DetailView):
"""Displays the details of a BlogPost"""
model = BlogPost
def get_queryset(self):
if self.request.GET.get("show_drafts"):
return BlogPost.objects.all()
else:
return BlogPost.objects.filter(published=True)
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 Django documentation on class-based views for a more complete introduction than I’ve given here.
- 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.
- The source code for the
django.views.generic
module is also a fun read. - Classy class-based views is an alternative set of documentation, which is particularly useful as a quick reference.
That’s all folks
I hope you enjoyed this look inside Django. Maybe we can do it again sometime?