Django tips: get the most out of generic views
Recently at work I had the chance to revisit an application I’d written fairly early on in my tenure at World Online; I was still getting used to doing real Django development and to some of the quirks of our development environment, and ended up writing a lot more code than I needed to, so I was happy to be able to take a couple days and rewrite large chunks of the application to be more efficient and flexible.
The biggest win was more careful use of generic views, which this particular application wasn’t making much use of originally. To put it into perspective, switching as many things as possible to generic views has meant that the application’s view code shrunk by about a third while gaining functionality.
In some situations that might not be much of a difference, but with an application that weighs in at a few thousand lines of code, shaving a full 33% off one of its components is a huge win, even if it just does the same things afterward, but in this case the application gained more functionality and flexibility. So let’s look through some things generic views can do to cut down the size of your code.
Vanilla views
The simplest case is when you find you’re writing code which does nothing but grab a plain list of objects from a model and display them. In this case you don’t have to write a view at all; the generic list_detail.object_list view will happily do that for you, and will throw in pagination for free (via the optional paginate_by
parameter).
Similarly, if you’re just showing a chronological archive of objects, the date-based generic views will probably be the only things you need; this blog, for example, runs almost completely on date-based generic views (even the home page uses one).
Of course, you already know that; the real thing I want to get at is how you can do more complex things with generic views, because that’s the thing a lot of people (myself included, once upon a time) tend to miss.
Extra filtering
Let’s suppose that you have a collaborative to-do list application, where each to-do list belongs to a specific user. You want to have a nice page that each user can go to and look at all of their own lists; for example, Bob should be able to go to the URL /lists/bob/
and see his to-do lists.
The (apparent) problem here is that it doesn’t look like we can tell the generic view about the filtering we want to do; the view has no way of “knowing” that it should look for a username in the URL, so we can’t rely on that. And while we could hard-code separate dictionaries of keyword arguments for each user, and set up a line in the URL configuration for each one (/lists/alice/
, /lists/bob/
, /lists/carol/
and so on), that doesn’t scale at all.
But we can do this with a generic view — we just have to give it a little help, in the form of a wrapper function. Way back in June, Malcolm wrote about how to do this, but it hasn’t gotten much attention. So let’s give some more attention to the technique, because it’s extremely powerful.
The first thing you’d do is add a line in your URL configuration like this:
('r^lists/(?P<username>\w+)/$', 'myproject.myapp.views.user_lists'),
This says that any URL matching /lists/<username>/
should go to the user_lists
view in your application. Now let’s see what that view looks like:
from django.views.generic.list_detail import object_list from myproject.myapp.models import TodoList def user_lists(request, username): todo_lists = TodoList.objects.filter(owner__username__exact=username) return object_list(request, queryset=todo_lists)
And you’re done.
The only thing holding this up was that the generic view wants to be passed a QuerySet
which contains the objects it’s supposed to filter or display, so all we’ve done here is write a two-line function which builds the correct QuerySet
(by reading the username out of the URL to do the filtering) and then calls the object_list
generic view with that QuerySet
as an argument.
This works because generic views — and, in fact, all views in Django — are just Python functions, so other functions can build up lists of arguments, call them and even return their responses directly. So any time you find yourself thinking “I could use a generic view for this if only I could somehow filter a little first”, remember that you can filter a little first by writing a short wrapper function that calls the generic view.
Getting more out of a generic view
Another common situation is needing to display a single object, or a list of objects — just the sorts of things generic views are good at — but also needing to add one or two extra things to show up in the template.
Again, it can be wrappers to the rescue: all of the generic views take an optional argument, extra_context
, which should be a dictionary of variables and values to pass through into the template context. Continuing with the to-do list example, let’s say we want to add a couple things to the view we wrapped above: the number of to-do items the user currently has open, and any high-priority items they have open.
Here’s the code:
from django.views.generic.list_detail import object_list from myproject.myapp.models import TodoList, TodoItem def user_lists(request, username): todo_lists = TodoList.objects.filter(owner__username__exact=username) open_items = TodoItem.objects.filter(todolist__owner__username__exact=username) open_item_count = open_items.count() priority_items = open_items.filter(priority__exact='high') return object_list(request, queryset=todo_lists, extra_context={'open_item_count': open_item_count, 'priority_items': priority_items})
And now the open_items
and priority_items
variables will be available, with the correct values, in the template.
Doing extra work
Another common pattern is needing to do take some sort of related action, like logging, before returning the final view. And again, it’s really easy to do this with a short wrapper around a generic view. If we had a user profile model which stored the last date/time users checked their to-do lists, we could once again add a couple lines to our wrapper and have it work:
import datetime from django.views.generic.list_detail import object_list from myproject.myapp.models import TodoList, UserProfile def user_lists(request, username): profile = UserProfile.objects.get(user_username__exact=username) profile.last_list_check = datetime.datetime.today() profile.save() todo_lists = TodoList.objects.filter(owner__username__exact=username) return object_list(request, queryset=todo_lists)
Now, each view of a user’s to-do lists will reset the last_list_check
field of their profile to the current date and time.
Learn it, love it, use it
There are tons of other uses for this technique, but hopefully I’ve got you thinking about them now and you’ll figure out how to use them in your next Django project. Writing wrappers like these around generic views when you need some extra processing is probably one of the most important and powerful idioms of everyday Django development; it can drastically cut down the amount of code you have to write and increase the options you have available (because even with just their stock set of options, generic views are pretty darned flexible).
Unfortunately, it seems to be one of the most-overlooked idioms as well; more than once I’ve explained this trick to people, even seasoned, experienced programmers, only to have their eyes go wide as they say, “wait, can you really do that?” You can do that, and you should whenever possible — the generic views are there, and built the way they are, for good reasons. Learning to use them effectively will save you time and effort on the path to writing better applications.