Andrew's Forge

Overcoming Troubles with Class-Based (Generic) Views

Published by

Written for Django 1.5

Abstract

This article is based on a 2013 DjangoCon presentation by the same name. The presentation and article cover the same material and aim to (re)introduce Class-Based Views (CBVs) and Class-Based Generic Views (CBGVs) to beginner and intermediate developers, first to make using both CBVs and CBGVs more practical and second to help developers understand some of the controversy surrounding their use. To these ends, this article will refactor a simple website to demonstrate CBV and CBGV usage.

The presentation and article assume basic knowledge of Django (official tutorial available here). The code is meant to demonstrate basic functionality and is not intended for production. It is available for download in its entirety on GitHub.

Article Back Story

In early 2013, I was asked by an Austin startup to prototype a banking webapp for usability testing. I saw this project as a golden opportunity to work with CBVs and CBGVs. Until this point, I had avoided using them in production code because of their reputation and because I was afraid to use a tool with which I did not have extensive experience in a client's production code. However, I felt pressured to learn all of Django, as I was on the verge of teaching an introductory course on the framework (now a forthcoming book from Addison-Wesley).

Thus began an adventure into Django's code to decipher how, exactly, CBVs and CBGVs operate. Rather than dissect in detail the process I took to learn CBVs and CBGVs, this article will step through the concepts in a more didactic fashion. I hope this article makes your discovery of CBV and CBGV less time consuming.

Introduction

A 'View' in Django is a function that takes an HTTP Request object, processes it, and returns an HTTP Response object. A Class-Based View (CBV) does exactly the same thing, but it is a class rather than a function. In keeping with its DRY (Don't Repeat Yourself) philosophy, Django provides a number of CBVs with preprogrammed behaviors and labels them Class-Based Generic Views (CBGVs). The implication is that while CBGVs are CBVs, CBGVs and CBVs are distinct Django features.

CBVs and CBGVs were both introduced in Django 1.3, released in March 2011. Since their inception, CBVs and CBGVs have been confused with one another, and have largely been vilified by Djangonauts. Russell Keith-Magee, President of the Django Foundation and the Django core developer who committed the final version to the repository, stated at DjangoCon EU 2013 that class-based views had "not been greeted with universal enthusiasm."(Slides to Russell's lecture may be found here.). Luke Plant, a Django core developer, wrote on his blog in May 2012 that "Django's CBVs were a mistake"; Harry Percival, author of O'Reilly's Test-Driven Web Development with Python allegedly stated at DjangoCon 2013 that he couldn't think of a reason to use CBVs. When I spoke to Jacob Kaplan-Moss about reorganizing the CBGV documentation in an attempt to make the subject clearer, he stated that the problem was not with the documentation but with the API itself.

Are CBVs and CBGVs really that bad? Does Django really supply a tool that offers no advantages? The only way to find out is to see exactly what the tools do and how they work.

The Initial Project and Code

Let's learn about CBVs and CBGVs by first building a basic site and then refactoring it to use the new concepts. Our basic site is a prototype banking app that provides four pages:

  • A list of (bank) accounts
  • A detailed account page, listing related transactions
  • A form for creating new accounts
  • A page for creating new transactions

We start by building two models in our new app. The Account model supplies an account with a balance, and the Transaction model details transfers between accounts.

# bank/models.py
from django.db import models
from django.db.models import Q
from django.core.urlresolvers import reverse

class Account(models.Model):
    name = models.CharField('Account Name', max_length=127)
    slug = models.SlugField(max_length=50, unique=True)
    balance = models.IntegerField('Balance')

    def __unicode__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('bank_account_detail',
                       kwargs={'slug': self.slug})

    def related_transactions(self):
        return Transaction._default_manager \
                          .filter(Q(from_account=self) |
                                  Q(to_account=self)) \
                          .order_by('date')

class Transaction(models.Model):
    amount = models.IntegerField('Amount')
    date = models.DateTimeField('Date', auto_now_add=True)
    from_account = models.ForeignKey(
                       Account,
                       related_name='transaction_from')
    to_account = models.ForeignKey(
                       Account,
                       related_name='transaction_to')

    def __unicode__(self):
        return ("Transfered $%s from %s to %s"
                % (self.amount, self.from_account,
                   self.to_account))

With our models now defined, we can create the related forms.

# bank/forms.py
from django.forms import ModelForm
from .models import Account, Transaction

class AccountForm(ModelForm):
    class Meta:
        model = Account

class TransactionForm(ModelForm):
    class Meta:
        model = Transaction

We can now code the URL configuration.

# bank/urls.py
from django.conf.urls import patterns, url
from .models import Account
from .views import (account_list, account_detail,
                    account_create, transaction_create)

urlpatterns = patterns('',
    url(r'^account/$',
        account_list,
        name='bank_account_list'),
    url(r'^account/create/$',
        account_create,
        name='bank_account_create'),
    url(r'^account/(?P<slug>[\w\-]+)/$',
        account_detail,
        name='bank_account_detail'),
    url(r'^transaction/$',
        transaction_create,
        name='bank_trans_create'),
)

Please keep in mind that (1) the URL name bank_account_detail is matched by the get_absolute_url() method in the Account model, and (2) the order of the URLs in our pattern matters. Consider that account/create/ is a valid match for account/(?P<slug>[\w\-]+)/; the first must therefore appear before the second. In a production setting, you would want to ensure that Account never accepts 'create' as a slug, typically by overriding the model's save() method.

Finally, we code the view functions.

# bank/views.py
from django.shortcuts import (get_object_or_404,
                              redirect, render)
from .models import Account
from .forms import AccountForm, TransactionForm

def account_list(request):
    acct_list = Account.objects.all()
    return render(request,
                  'bank/account_list.html',
                  {'account_list': acct_list})

def account_detail(request, slug):
    acct = get_object_or_404(Account, slug=slug)
    return render(request,
                  'bank/account_detail.html',
                  {'account': acct})

def account_create(request):
    if request.method == 'POST':
        form = AccountForm(request.POST)
        if form.is_valid():
            new_acct = form.save()
            return redirect(new_acct)
    else:
        form = AccountForm()
    return render(request,
                  'bank/account_form.html',
                  {'form': form})

def transaction_create(request):
    if request.method == 'POST':
        form = TransactionForm(request.POST)
        if form.is_valid():
            new_trans = form.save()
            return redirect('bank_account_list')
    else:
        form = TransactionForm()
    return render(request,
                  'bank/transaction_form.html',
                  {'form': form})

We may access the objects passed to the account_list() and account_detail() templates using the account_list and account variables, respectively. Similarly, the account_create() and transaction_create() templates access the form via the creatively named form variable. To finish, we can add data to the database using the Django shell, after running $ python manage.py syncdb as below.

$ python manage.py shell
...
>>> from bank.models import Account, Transaction
>>> b = Account.objects.create(
... name='Basic', slug='basic', balance=200)
>>> s = Account.objects.create(
... name='Savings', slug='savings', balance=4000)
>>> Transaction.objects.create(
... amount=20,from_account=s, to_account=b)
<Transaction: Transfered $20 from Savings to Basic>
>>> Transaction.objects.create(
... amount=2,from_account=b, to_account=s)
<Transaction: Transfered $2 from Basic to Savings>
>>> Transaction.objects.create(
... amount=40,from_account=s, to_account=b)
<Transaction: Transfered $40 from Savings to Basic>

After creating some simple templates (available on GitHub), we now have a very simple bank prototype. We can now begin to refactor to see how Class Based Views change our code.

Refactoring Views into Class-Based Views

A CBV is simply a class that inherits from View, which is an abstract class provided by Django. (Abstract classes are not meant to be instantiated and are instead intended only as super classes, facilitating development by bequeathing appropriate behaviors to subclasses.)

We thus begin our refactor by adding from django.views.generic import View to the list of imports in our banks/views.py file.

We are now free to switch over our function views to CBVs. Let's start with the account_list and account_detail views and change our function into a class. We refactor the code below ...

# bank/views.py
def account_list(request):
    acct_list = Account.objects.all()
    return render(request,
                  'bank/account_list.html',
                  {'account_list': acct_list})

... into the following:

# bank/views.py
class AccountList(View):
    def get(self, request):
        acct_list = Account.objects.all()
        return render(request,
                      'bank/account_list.html',
                      {'account_list': acct_list})

We declare a subclass of our newly imported View class and then define a method to handle the page. The code in this method is identical to the code in the original function view (except for the self parameter, which must be the first parameter in any method). As before, the method accepts an HTTP Request object and returns an HTTP Response object.

We follow the same process with account_detail, so that the code below ...

# bank/views.py
def account_detail(request, slug):
    acct = get_object_or_404(Account, slug=slug)
    return render(request,
                  'bank/account_detail.html',
                  {'account': acct})

... becomes the following:

# bank/views.py
class AccountDetail(View):
    def get(self, request, slug):
        acct = get_object_or_404(Account, slug=slug)
        return render(request,
                      'bank/account_detail.html',
                      {'account': acct})

Again, the logic has not changed.

In both cases, we've named our view method get(). By convention, the name of our method has meaning for the CBV. If the page is requested with an HTTP GET request, then the get method will be called on a CBV. Any method in a CBV with the name of an HTTP verb will be called when that verb requests the page. Should our CBV not have a method named after an HTTP verb, such as get or post, then our CBV cannot be called and will not run. Django will only call methods with an HTTP verb name. Not all methods in a CBV must be named after an HTTP verb. We can (and will) write methods with other names—as we would in basic Python—which we will call from the methods named after verbs (verb methods).

This convention becomes clearer in the context of a view with a form. As before, we take the view below ...

# bank/views.py
def account_create(request):
    if request.method == 'POST':
        form = AccountForm(request.POST)
        if form.is_valid():
            new_acct = form.save()
            return redirect(new_acct)
    else:
        form = AccountForm()
    return render(request,
                  'bank/account_form.html',
                  {'form': form})

... and refactor it into the following:

# bank/views.py
class AccountCreate(View):
    def get(self, request):
        form = AccountForm()
        return render(request,
                      'bank/account_form.html',
                      {'form': form})

    def post(self, request):
        form = AccountForm(request.POST)
        if form.is_valid():
            new_acct = form.save()
            return redirect(new_acct)
        else:
            return render(request,
                          'bank/account_form.html',
                          {'form': form})

In the original function view, the first if statement checked the request object to see which HTTP verb had requested the page. Based on whether it was a POST or GET request, the function view then either attemptted to process the page or simply output the form. In the case of the CBV, each HTTP verb is now processed by a different method, so an if statement is both unnecessary and fundamentally impossible. We therefore use a different method for the logic in each branch of the first if statement.

Using the same principle, we refactor the code below...

# bank/views.py
def transaction_create(request):
    if request.method == 'POST':
        form = TransactionForm(request.POST)
        if form.is_valid():
            new_trans = form.save()
            return redirect('bank_account_list')
    else:
        form = TransactionForm()
    return render(request,
                  'bank/transaction_form.html',
                  {'form': form})

... into the following:

# bank/views.py
class TransactionCreate(View):
    def get(self, request):
        form = TransactionForm()
        return render(request,
                      'bank/account_form.html',
                      {'form': form})

    def post(self, request):
        form = TransactionForm(request.POST)
        if form.is_valid():
            new_acct = form.save()
            return redirect('bank_account_list')
        else:
            return render(request,
                          'bank/account_form.html',
                          {'form': form})

For a little bit of extra code, a CBV offers exceptional clarity by splitting HTTP verbs into separate methods. This is arguably more Pythonic, as the language prides itself on explicit code.

Our refactor is now nearly—but not quite—complete. Now that we are no longer using function views, we must change our URL configuration as well. The new one appears below.

# bank/urls.py
from django.conf.urls import patterns, url
from .models import Account
from .views import (AccountList, AccountDetail,
                    AccountCreate, TransactionCreate)

urlpatterns = patterns('',
    url(r'^account/$',
        AccountList.as_view(),
        name='bank_account_list'),
    url(r'^account/create/$',
        AccountCreate.as_view(),
        name='bank_account_create'),
    url(r'^account/(?P<slug>[\w\-]+)/$',
        AccountDetail.as_view(),
        name='bank_account_detail'),
    url(r'^transaction/$',
        TransactionCreate.as_view(),
        name='bank_trans_create'),
)

There are two notable changes to the URL configuration. First, the import from bank/views.py is completely changed. Second, all of the functions being mapped to have also been changed. The code no longer points to account_list but instead points to AccountList.as_view(). Rather than simply pointing to the CBV, we are pointing to the output of a method inherited via the View class. We will examine exactly why we are doing this when we examine CBV internals, but consider for a moment the nature of URL mapping: a URL configuration maps a URL to a callable. We thus know that the value returned by as_view() is a callable.

Strengths and Advantages of Class-Based Views

Even though our code now uses CBVs, we are not taking advantage of their real strengths. Consider our two newest class views, and note how both of them share an enormous amount of code (just as function views do). Both get() and post() on either class do the same thing, with the exception of (1) which form is used, (2) which template is loaded, and (3) how the view redirects upon success. If we turn these three exceptions into instance variables, we actually have the same code, which we could move into a new abstract mix-in class, below.

# bank/views.py
class FormMixin(object):
    form = None
    template = ''
    redirect = ''

    def get(self, request):
        form = self.form()
        return render(request,
                      self.template,
                      {'form': form})

    def post(self, request):
        form = self.form(request.POST)
        if form.is_valid():
            new_obj = form.save()
            if self.redirect:
                return redirect(self.redirect)
            else:
                return redirect(new_obj)
        else:
            return render(request,
                          self.template,
                          {'form': form})

We may now refactor our CBVs to inherit from our mix-in class. Inheriting FormMixin provides us with the get() and post() methods we just programmed; we need only override the appropriate instance variables.

# bank/views.py
class AccountCreate(FormMixin, View):
    form = AccountForm
    template = 'bank/account_form.html'

class TransactionCreate(FormMixin, View):
    form = TransactionForm
    template = 'bank/account_form.html'
    redirect = 'bank_account_list'

We set form and template in both cases, but we need only specify redirect in TransactionCreate, as Transaction objects do not have a get_absolute_url() method, whereas Account objects do.

CBVs offer the ability to easily and flexibly deduplicate code via class inheritance. However, our example above illustrates a drawback to this system: a programmer writing a subclass must know about the internals of the superclass. A programmer must know which instance variable to override and which not to.

To help circumvent errors related to this drawback, we can introduce methods to check for the existence of instance variables and to output errors directed at the developer in the event of a problem. This behavior is common practice when programming on large systems, and Django uses it internally. Below, we introduce two functions to verify the existence of template and redirect and could easily do the same for form.

# bank/views.py
class FormMixin(object):
    ...

    def get_template(self):
        if self.template == '':
            raise ImproperlyConfigured(
                '"template" variable  not defined in %s'
                % self.__class__.__name__)
        return self.template

    def get_redirect_url(self,obj):
        if self.redirect:
            url = self.redirect
        else:
            try:
                url = obj.get_absolute_url()
            except AttributeError:
                raise ImproperlyConfigured(
                    '"redirect" variable must be defined '
                    'in %s when redirecting %s objects.'
                    % (self.__class__.__name__,
                       obj.__class__.__name__))
        return url

The first method checks for the existence of an instance variable and returns it if it exists. If it does not exist, then the method raises an ImproperlyConfigured exception, letting the developer know that he or she has forgotten to set the template variable in the appropriate class. The second method does much the same for the redirect variable but takes the extra step to see whether the object passed has a get_absolute_url() method, which allows a developer to leave redirect unset when using the view on models which have implemented get_absolute_url().

Our verb methods, get and post, must now be refactored to call our new methods, get_template and get_redirect_url, rather than accessing the instance variables directly.

# bank/views.py
class FormMixin(object):
    ...

    def get(self, request):
        form = self.form()
        return render(request,
                      self.get_template(),
                      {'form': form})

    def post(self, request):
        form = self.form(request.POST)
        if form.is_valid():
            new_obj = form.save()
            return redirect(self.get_redirect_url(new_obj))
        else:
            return render(request,
                          self.get_template(),
                          {'form': form})

The variables self.template and self.redirect become self.get_template() and self.get_redirect_url(new_obj), respectively.

Class-Based Generic Views

The FormMixin class we have written is actually unnecessary. In keeping with its DRY (Don't Repeat Yourself) philosophy, Django has anticipated the natural extension of our example: any large project is going to need to supply a slew of classes similar to ours to create basic functionality. To avoid programmers recreating the wheel with every project, Django includes a number of classes that developers can use or inherit from. These classes are known as Class-Based Generic Views.

Django’s online CBGV documentation explains many of these views. Notably, ListView, DetailView and CreateView are all views that we can simply inherit from as long as we define a model variable (and a success_url in the case of our TransactionCreate view, as Transaction does not have a get_absolute_url). This not only shortens the code in bank/views.py but also allows us to delete bank/forms.py.

# bank/views.py
from django.core.urlresolvers import reverse_lazy
from django.views.generic import (ListView, DetailView,
                                  CreateView)
from .models import Account, Transaction

class AccountList(ListView):
    model = Account

class AccountDetail(DetailView):
    model = Account

class AccountCreate(CreateView):
    model = Account

class TransactionCreate(CreateView):
    model = Transaction
    success_url = reverse_lazy('bank_account_list')

Amazingly, no further changes are required. You may have anticipated needing to change the variables in the templates from account_list and account to object_list and object (respectively), but Django accommodates the migration by setting both context variable names to the values passed (i.e., account and object are both set).

These changes are just the tip of the iceberg, however. The documentation also reveals that—rather than defining views as above—the URL configuration can simply use the generic views, passing in the necessary variable overrides as arguments to as_view(), as demonstrated below.

# bank/urls.py
from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse_lazy
from django.views.generic import (ListView, DetailView,
                                  CreateView)
from .models import Account, Transaction

urlpatterns = patterns('',
    url(r'^account/$',
        ListView.as_view(model=Account),
        name='bank_account_list'),
    url(r'^account/create/$',
        CreateView.as_view(model=Account),
        name='bank_account_create'),
    url(r'^account/(?P<slug>[\w\-]+)/$',
        DetailView.as_view(model=Account),
        name='bank_account_detail'),
    url(r'^transaction/$',
        CreateView.as_view(model=Transaction,
                           success_url=reverse_lazy(
                               'bank_account_list')),
        name='bank_trans_create'),
)

This code replaces the views we defined in bank/views.py, effectively allowing us to delete the file. The URL configuration now calls the CBGVs directly rather than a subclass. The arguments of as_view() are the values we had previously defined as instance variables.

Complications

When developers begin to work with CBVs and CBGVs, this last example (where the CBGVs are being directly called in the URL configuration) is typically where they start. This is the method detailed in the Django Tutorial, and it is arguably the cleanest and simplest way to use CBGVs, which is a huge incentive for using this method.

However, an implication of starting with this method is that developers are not aware of the basic underlying structure and options that we have explored in this article (and we have only scratched the surface). They are not aware of the difference between CBVs and CBGVs, unaware that View is the underlying base for the system, and probably uncertain of how best to find information about these classes.

This ignorance generally does not cause problems when using CBGVs in URLs, or even when overriding CBGV instance variables, as in the example before last. The Django documentation carefully details the instance variables of each CBGV, including information about which superclass actually defines the value. This is how a developer might discover the necessity of specifying the values of model or success_url as above (as opposed to reading through error pages, as I did).

Unfortunately, rarely are Django developers happy with functionality straight out of the box. Consider our bank app again. With our CBGVs working as desired, let's polish the site. Our new tasks are to:

  • display both Accounts and Transactions on the front page;
  • automate and hide slug creation;
  • Split TransactionCreate into two forms (each Account will have two links):
    • Send To: the To field is already set to the desired account;
    • Send From: the From field is already set to the desired account.

None of these changes may be achieved by changing instance variables. Instead, developers must turn their attention to overriding methods. Even though this is documented on the official site, the information doesn't provide code, which is really what developers need to examine.

At this stage, however, it makes little sense to examine the code for CBGVs, as we still do not understand what they are built on. Let's instead examine the source code for View.

Class-Based View Internals

NB: Django source code presented here has been formatted to make reading easier (comments are removed, certain strings are shortened, etc).

All CBV and CBGV inherit from the View class found in django/views/generic/base.py. We've inherited View in CBVs of our own, and we know that it provides the as_view() method, which is used to map URLs in the URL configuration. Let's begin our examination of the class there.

Quick reminder: the value passed to a URL mapping must be callable.

# django/views/generic/base.py
@classonlymethod
def as_view(cls, **initkwargs):
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError('...')
        if not hasattr(cls, key):
            raise TypeError('...')

    def view(request, *args, **kwargs):
        self = cls(**initkwargs)
        if hasattr(self, 'get') and not hasattr(self, 'head'):
            self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)

    update_wrapper(view, cls, updated=())
    update_wrapper(view, cls.dispatch, assigned=())
    return view

The as_view() method is actually a factory. It first processes any arguments passed to it and then defines a nested view() method. Before finally returning this method, it matches the class’s docstrings and decorators to the method using functools.update_wrapper(). For example, when CreateView calls as_view(), view() is given CreateView’s docstring and any decorators applied to CreateView.

The view() method is thus aptly named, as it is the function the URL configuration maps to . It accepts a request argument and has general *args and **kwargs parameters for arguments passed in via the URL or the optional dictionary. However, it does not behave as one might expect. Notably, view() does three distinct things:

  1. view() instantiates the class it belongs to with the arguments passed to as_view(); arguments passed to TransactionCreate.as_view() are passed to TransactionCreate.__init__(). Arguments passed to as_view() may thus override instance variables (but not create new ones, as as_view() weeds those out in its first loop).
  2. view() assigns its own arguments (request *args and **kwargs) to instance variables (of the same name) of the object instantiated in 1.
  3. view() returns a call to the objects' dispatch() method.

Aside: #1 illustrates how we were able to pass instance variables to as_view() when using CBGVs.

With the behavior of as_view() and view() firmly established, let's look at dispatch(). The dispatch() method determines which HTTP verb has requested the view and returns a call to the method by the same name (or else returns an error). For example, if the page is requested by HTTP GET, dispatch will return a call to get().

# django/views/generic/base.py
def dispatch(self, request, *args, **kwargs):
    if request.method.lower() in self.http_method_names:
        handler = getattr(self, request.method.lower(),
                          self.http_method_not_allowed)
    else:
        handler = self.http_method_not_allowed
    return handler(request, *args, **kwargs)

In short, a call to as_view might be mapped as:
as_view()->view()->dispatch()->get()

View defines the names of acceptable HTTP verbs in the instance variable http_method_names. Specifically, get(), post(), put(), delete(), head(), options() and trace() are all valid method names identified for dispatch(). If head() is not defined, view() will automatically handle head requests using the get method (if defined). The options() method is defined in View and simply passes back the list of valid HTTP verbs the view accepts. In the event that a page is requested with an HTTP verb not defined in http_method_names, the CBV will call http_method_not_allowed(), which returns a HTTP 405 error.

The lifetime, or state, of a CBV object is thus the length of a single request. The CBV is instantiated in view() shortly after as_view() is called by the URL configuration. Every HTTP request instantiates a new object (if the view is a CBV). Once a CBV returns the HTTP response object, it has served its purpose and may be garbage collected. This is good news for the security of a Django web app, as it makes it impossible (or at least very difficult) to mix the states of two HTTP requests.

Exploring Class-Based Generic Views

With a fundamental understanding of the superclass for all CBGVs, we can now examine these classes. However, with sixteen concrete classes and twenty-nine mix-in classes, there is no obvious place to start peering into this very complicated hierarchy.

How complicated? Here is a graph of all CBGVs, all the way up to object. Each subclass points to its superclass(es).

Graph of all CBGVs We can try to clean it up, but doing so is not an obvious task. Organizing the classes by file doesn't help.

Graph of all CBGVs organized by files Let's just line up all the ones we plan to use on the side.

Graph of all CBGVs organized hierarchically We can now follow the inheritance structure of the CBGVs pretty closely, but the graph begs the question: how does Python handle multiple inheritance? When a class inherits from multiple classes with the same instance variable or method, how does Python choose which one to call?

Method Resolution Order

In version 2.2, Python introduced a new class model. The "new-style classes" did not replace the "classic classes," but instead offered an alternative, allowing for backward compatibility of unmodified code.

Classic classes could be defined as before: class PythonClass()
New-style classes could now be defined like so: class PythonClass(object)

The intent of the new-style classes was to standardize and unify the way types and classes are handled across the language. This standardization notably introduced descriptors and attributes, establishing methods for introspecting and manipulating objects.

On top of these changes, new-style classes also set out to automatically establish method resolution order (MRO), which is used in multiple inheritance contexts and dictates which method or attribute is called by a subclass when several superclasses define a method by the same name and signature (name and number of parameters).

For example, consider the following code. Which variable is accessed when D.x is called? Specifically, what is the value of Z.x?

class A(object): x = True
class B(object): x = False
class Z(A, B): pass

Z.x is True, and it becomes easy to see why as soon as we understand how Python handles MRO. Typically, MRO operates by linearizing a multiple inheritance scheme. The graphs below demonstrate this concretely: Python 2.7's MRO algorithm handles the inheritance scheme on the left according to the graph on the right. This linearization makes it evident which method or variable is called by the subclass: it always calls its youngest ancestor.

Demonstration of MRO While the MRO introduced by new-style classes in 2.2 was an improvement, developers discovered that the new method was not acting as expected. Specifically, order was not being preserved monotonically during the linearization process. At the suggestion of Samuele Pedroni on the Python mailing list, Python replaced the method with a new MRO called C3, which was originally developed for the Dylan programming language (the example above uses this algorithm).

The effects of this change are most apparent when considering a diamond inheritance scheme.

class A(object): x = 'False'
class B(A): pass
class C(A): x = 'True'
class Z(B, C): pass

Diamond Inheritance Scheme Graph In the new-style class case, Z.x will yield True, which is what most programmers anticipate. However, in the classic class case, where A does not inherit from object, Z.x yields False. (Examples from StackOverflow and a blog post by the creator of Python, Guido van Rossum.)

Understanding that all CBVs inherit from object—and are thus subject to new-style class inheritance schemes defined by the C3 MRO algorithm—allows us to easily determine how methods and variables are called or accessed across the complicated graph of classes that make up CBGVs. As demonstrated before, we can determine the linear order in which superclasses are checked for these attributes. The mro() method bundled with new-style classes allows developers to simply ask Python for the relevant information.

$ cd any/django/project/
$ ./manage.py shell 
>>> from django.views.generic import ListView
>>> from pprint import pprint
>>> pprint(ListView.mro())
[<class 'django.views.generic.list.ListView'>,
 <class 'django.views.generic.list.MultipleObjectTemplateResponseMixin'>,
 <class 'django.views.generic.base.TemplateResponseMixin'>,
 <class 'django.views.generic.list.BaseListView'>,
 <class 'django.views.generic.list.MultipleObjectMixin'>,
 <class 'django.views.generic.base.ContextMixin'>,
 <class 'django.views.generic.base.View'>,
 <type 'object'>]

This simplifies how much running around in the code base we have to do because we now immediately know the order of the classes to check when searching for a value or function.

A Clearer Picture

With a fundamental understanding of View—the basis of all CBVs—as well as how Python handles multiple inheritance for new-style classes, we can now come back to our graph of all CBGVs and simplify it by removing references to object and View, which we now assume to be the top two superclasses for all CBGVs. We can then color the classes according to the file in which they reside. The new graph looks like this:

Graph of all CBGVs Recall that to customize a CBGV we must override specific methods, but we are unsure of which method we need to override or where this method might exist. At this stage, we could continue to document each class individually in our graph, specifying the variables and methods defined in each, as well as which superclass defines each attribute, making it possible to find at a glance the attribute we want. (Unlike our treatment of View and object, which is necessary to understand CBGVs, the outline of the attributes of each class would act as reference.)

Fortunately, we do not need to do build this reference guide, as it has been done for us by Refresh Oxford and published online at ccbv.co.uk. The site is actually more useful than the reference guide described above because it allows for direct perusal of code. Rather than using a reference to open the file, find the class, and then read the code of the desired method, the website provides the code is provided directly, saving you time when hunting for the right method to override.

We now have the knowledge needed to understand any CBGV and a tool to rapidly discover code. We are ready to customize our CBGVs.

Customizing Class-Based Generic Views

The tasks we have set out to accomplish are:

  • display both Accounts and Transactions on the front page
  • automate slug entry (first by automating creation if left empty and then by removing the form field entirely)
  • Split TransactionCreate into two forms, so that each Account would have two links/buttons:
    • Sent To: the To field is already set to the desired account
    • Send From: the From field is already set to the desired account

Listing Transactions with Accounts

If we head to ccbv.co.uk we can view all of the methods of the ListView class, which we are currently using to display the list of Account. We know that we are trying to affect the display of a simple page display, so our knowledge of View tells us to jump to the get() method on the page. Reading through the method, we discover the existence of get_context_data(), the result of which is being passed as context data to the template via render_to_response(). The site further shows that get_context_data() is defined in two superclasses, MultipleObjectMixin and ContextMixin (the first calls the second via a call to super()); both implementations are available to peruse. This quickly allows us to determine that this is the class we want to override.

We can override the method in our bank/views.py using the following:

# bank/views.py
class AccountList(ListView):
    model = Account

    def get_context_data(self, **kwargs):
        context = super(AccountList, self)\
                       .get_context_data(**kwargs)
        context['transaction_list'] = Transaction\
                                          .objects\
                                          .order_by('-date')[:10]
        return context

We (1) get the context data as it would be passed to get() without our override, (2) add our extra context variable, and then (3) return it.

A quick modification to our template allows us to display the latest transactions alongside our Account list.

Automating the Creation of slug Values

For usability reasons, we would like to automate and hide the creation of the slug when creating a new account. There are many ways to do this, including creating a custom field or overriding the model's save() method. For the purposes of this article, we are going to do it in the view and form levels of our application.

At this point, we have deleted bank/forms.py because CBGVs automatically generate forms using ModelForm. However, we want to modify the form both in function and visually, so we need to recreate the file and manually manage the form used to create accounts. In turn, we will inform the appropriate view to use this form rather than generate one. We won't actually need to make any other changes in our CBGV, however, since we are going to handle the form behavior directly in bank/forms.py.

A quick review of the form Validation documentation online reveals that we can automatically override clean() to use the value of one field to set the value of another. This allows us to create a new bank/forms.py file and write the following:

# bank/forms.py
from django.forms import ModelForm
from django.utils.text import slugify
from .models import Account

class AccountForm(ModelForm):
    class Meta:
        model = Account

    def clean(self):
        cleaned_data = super(AccountForm, self).clean()
        name = cleaned_data.get("name")
        slug = cleaned_data.get("slug")

        if not slug and name:
            cleaned_data['slug'] = slugify(name)

        return cleaned_data

To ensure that AccountForm doesn't reject an empty field, we must add blank = True to the list of parameters of our slug field, as below.

# bank/models.py
class Account(models.Model):
    ...
    slug = models.SlugField(max_length=50, unique=True, blank=True)

We can then override the form used by the view either in bank/views.py directly or in the URL configuration, below.

# bank/urls.py
url(r'^account/create/$',
    CreateView.as_view(model=Account,
                       form_class=AccountForm),
    name='bank_account_create'),

While this process automates the creation of the slug value, the actual field widget is still visible to the user. In the Meta class of our form, we can add exclude = ('slug',) to make it disappear from the form. However, an unintended consequence is that the form no longer saves any value to slug. The exclude variable removes the field entirely from processing, including from the save() method.

The ModelForm class our form inherits exists in django-src/forms/models.py and inherits from ModelFormMetaclass and BaseModelForm. The latter defines the save() method, which calls the save_instance() function. The save_instance() calls construct_instance() to instantiate an object and then attaches the nested function save_m2m() to the new object, returning this new construction to save(). The construct_instance() function is the culprit, actively excluding the slug value, even if it is set in cleaned_data.

We can’t override save_instance() or construct_instance() as they are functions (rather than methods) that exist solely in django-src/forms/models.py. We thus override the save() method, taking into account save_m2m() as a way to handle many-to-many relationships.

# bank/forms.py
class AccountForm(ModelForm):
    class Meta:
        model = Account
        exclude = ('slug',)

    def save(self, commit=True):
        instance = super(AccountForm, self)\
                        .save(commit=False)
        instance.slug = slugify(
                            self.cleaned_data\
                                .get('name', ''))
        if commit:
            instance.save()
            self.save_m2m()
        return instance

We grab the instance returned by the superclass's save(), making sure to set commit = False, so that the instance is not saved to the database. We then add the desired slug value and, if commit is True, save the instance and apply the many-to-many relations. Finally, we return the instance as expected.

This section may not involve CBVs or CBGVs, but this rapid review of validation will help us avoid using exclude to hide an element in the next section. Another note is that CBGVs are not the only intricate part of Django. Used in unusual ways, ModelForm proves to be just as tricky with regard to customization.

Splitting Transactions

Our current TransactionForm displays three fields:

  • To Account (drop-down menu widget)
  • From Account (drop-down menu widget)
  • Amount (text input widget)

Instead of a single form page, we would like two pages, one in which 'To Account' is already preset and another in which 'From Account' is already preset. Each account detail page would have two new links: ‘Send Funds To,’ ‘Send Funds From.’ For clarity, each form page would list the account affected (i.e., in the case of the ‘Send Funds To’ page, where only the ‘From Account’ and ‘Amount’ widgets are needed, we will display which account is being sent funds in order to make the form more user-friendly).

We could handle each case by creating two forms and then two views. While there is no way to meet our goal with a single form, it is possible to deduplicate the view code by using a single CBGV.

We will determine which account to use and which form to display based on the URL scheme. Specifically, we will use the account slug to determine which account is being affected.

The first approach we will consider uses the exclude option to remove the undesired widgets in each form and introduce the missing value in a post()-related method. However, as we know from the previous section, this would require overriding the form's save() function, not only to save the instance but also so that our CBGV may pass the account information found in the slug. While this is a valid method, we are going to treat it as undesirable and look to minimize the code in the forms.

We therefore know that we cannot exclude the field and must instead hide the widget using a hidden field. This naturally leads us to consider passing the appropriate account data as initial data, which will mirror our override to introduce the name of the Account to the template.

Using ccbv.co.uk, we can trace the methods called by get() and discover the get_initial() and get_context_data() methods, both of which suit our purposes for modifying the CBGV.

With these goals firmly in place, we can begin to code. Before we can override these methods, we must first create the forms for our views to use.

# bank/forms.py
class TransferFormFrom(ModelForm):
    class Meta:
        model = Transaction
        widgets = {
            'from_account': HiddenInput(),
        }

class TransferFormTo(ModelForm):
    class Meta:
        model = Transaction
        widgets = {
            'to_account': HiddenInput(),
        }

We can now append our new URL mappings to our URL configuration. We use the same CBV (our anticipated override of a CBGV) and set the form_class variable to use our two new forms.

# bank/urls.py
url(r'^account/from/(?P<slug>[\w\-]+)/$',
    TransactionCreate.as_view(form_class=TransferFormFrom),
    name='bank_trans_from'),
url(r'^account/to/(?P<slug>[\w\-]+)/$',
    TransactionCreate.as_view(form_class=TransferFormTo),
    name='bank_trans_to'),

Before we program our custom get_initial() and get_context_data(), we first need the account data to set in these methods. The easiest way to access this data is by new instance variables, as this avoids having to add parameters to either get_initial() or get_context_data(), further allowing us to leave the flow of the CBGV alone. The method below requires a URL slug as an argument and uses it to find the related Account.

# bank/views.py
class TransactionCreate(CreateView):
    model = Transaction

    def set_account(self, slug):
        account_obj = get_object_or_404(Account,\
                                        slug__iexact=slug)
        self.acct_name = account_obj.name
        self.acct_pk = account_obj.pk
        self.acct_url = getattr(account_obj,\
                                'get_absolute_url', '')

Using slug, the method queries the database for the proper account object and uses it to get the name of the account, the primary key of the account, and a pointer to its get_absolute_url() method.

We now need to call set_account() in a method that has access to the slug value passed to the view. Based on our knowledge of View, we know that as_view() will create view(), to which the URL configuration will pass the request and keyword arguments containing the slug. view() will pass these arguments to dispatch() and then to get() or post(), both of which may make calls to our overridden methods. We cannot override view, as doing so would require overriding as_view(). Overriding dispatch() is appealing (and what I did originally when I presented this talk) because it offers a single simple place to do so, but this defies the logic of dispatch(). Instead, it is best to call set_account() in overrides of both get() and post().

# bank/views.py
class TransactionCreate(CreateView):
    ...

    def get(self, request, *args, **kwargs):
        self.set_account(kwargs.get('slug', None))
        return super(TransactionCreate, self)\
                    .get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.set_account(kwargs.get('slug', None))
        return super(TransactionCreate, self)\
                    .post(request, *args, **kwargs)

Now that our class has the data that we need, we can override get_initial() and get_context_data() as follows:

# bank/views.py
class TransactionCreate(CreateView):
    ...

    def get_initial(self):
        initial_data = super(TransactionCreate, self)\
                            .get_initial()
        if self.form_class == TransferFormFrom:
            initial_data['from_account'] = self.acct_pk
        elif self.form_class == TransferFormTo:
            initial_data['to_account'] = self.acct_pk
        else:
            raise ImproperlyConfigured(
                        '"form_class" variable must be defined '
                        'in %s for correct initial behavior.'
                        % (self.__class__.__name__,
                           obj.__class__.__name__))
        return initial_data

In get_initial(), we first get the initial data as created by the CGBV without our override by calling the superclass's version of get_initial(). We then check which form we are presenting and set the appropriate (hidden) field to the value of the account for which we are displaying the form. If form_class is not set, we make sure to let the developer know there is a problem by raising an exception. Finally, we return initial_data, as get() anticipates its return.

# bank/views.py
class TransactionCreate(CreateView):
    ...

    def get_context_data(self, **kwargs):
        context = {'account_name': self.acct_name}
        if self.form_class == TransferFormFrom:
            context['trans_dir'] = 'From:'
        elif self.form_class == TransferFormTo:
            context['trans_dir'] = 'To:'
        else:
            raise ImproperlyConfigured(
                        '"form_class" variable must be defined '
                        'in %s for correct initial behavior.'
                        % (self.__class__.__name__,
                           obj.__class__.__name__))
        context.update(kwargs)
        return super(TransactionCreate, self)\
                    .get_context_data(**context)

Our get_context_data is very similar to get_initial() in that we set and return the data after a call to the superclass, making sure to warn the developer if form_class is not defined. However, superclass versions of get_context_data() follow a different convention than get_initial(): subclasses pass overridden context values to superclasses via super. As such, we first create and add to a new context variable and then pass it to the superclass version of get_context_data() which returns the full dictionary of context values. We finish by returning this dictionary.

For good measure, we also override get_success_url().

# bank/views.py
class TransactionCreate(CreateView):
    ...

    def get_success_url(self):
        if (hasattr(self, 'acct_url')
           and self.acct_url != ''):
            return self.acct_url()
        else:
            return reverse('bank_account_list')

We check for the existence of the acct_url instance variable and make sure it is not empty. If it exists and is set, we have a pointer to get_absolute_url() on the desired Account model. We therefore return a call to acct_url, effectively passing the return value of get_absolute_url(). In the event it does not exist (this should never be the case but ensures code robustness), we simply redirect to the list of accounts.

And with these fifty-eight lines of code, we have successfully overridden the CreateView CBGV to get it to work exactly the way we want it to.

Conclusion

While Class-Based Generic Views are intricate, Class-Based Views are quite simple.

Class-Based Generic Views

CBGVs are sometimes referred to as ravioli code: ravioli are easy to eat but difficult to modify once cooked. Calling and using CBGVs is quite simple, but being able to manipulate a CBGV takes more work. Furthermore, controlling a CBGV requires a fundamental understanding of MRO and View as well as a tool such as ccbv.co.uk to help rapidly read and find code. This tool is important because remembering all of the different CBGVs is difficult and should be unnecessary.

As such, CBGVs are great tools for rapidly prototyping a website.

It’s worth noting that while CBGVs have drawn criticism for being overly complex and difficult to customize, few have made the same criticism of other, similar parts of Django, such as ModelForm, which has proved to be just as complex as CBGVs.

Class-Based Views

CBVs are an overlooked resource. They offer the same level of granular control as a function view but provide explicit control of HTTP verb handling, which is arguably more Pythonic. Furthermore, CBVs are more in keeping with Django philosophy, as the rest of the framework, such as Models and Forms, are already classes. These advantages, on top of the ability to inherit from other classes, make CBVs powerful without any loss of control.

Part of the confusion surrounding CBVs stems from the fact that View, the basis for CBVs, is actually (as of the date of this article, October 5, 2013) listed in the documentation alongside the CBGVs, causing developers both old and new to continue to lump the two systems together. Readers of this article should now know that the two are different.

Final Thoughts

Ravioli are a sometimes food. There are plenty of good reasons for and against CBGVs depending on your situation, but there is no reason not to use CBVs. Future Django projects should seriously consider using them instead of function views.

I hope this article helps clarify the differences between CBVs and CBGVs and makes both more accessible to your development process.