Explaining Molly’s class-based views

Note

This has been lifted verbatim from a blog post and still needs to be tidied up to fit the documentation.

When an HTTP request is handled by a Django website, it attempts to match the local part of the URL against a series of regular expressions. Upon finding a match it passes an object representing the request as an argument to a callable associated with the regular expression. In Python, most callables you will find are class or instance methods, or functions. The Django documentation only briefly refers to the fact that one can use callables other than functions.

The flow of a request

Django despatches an incoming request to the callback given in a urlconf which, in Molly, is a class object. Calling a class object is mapped to calling it’s __new__ method. Ordinarily, this returns an instance of that class, but in Molly it returns the response. Specifically:

class FooView
__new__(request, *args, **kwargs):
Parameters:request (HttpRequest) – The request from the client to be processed.
Return type:HttpResponse

Unless overridden, the method called is molly.utils.views.BaseView.__new__(). This performs the following steps:

  • Checks there is a handler available for the HTTP method specified. If not, it immediately returns a 405 Method Not Acceptable response.
  • Calls cls.initial_context(request, *args, **kwargs), which can provide context for all method handlers. A developer can use this to factor out code common between each of the handlers.
  • Evaluates the breadcrumbs and adds the resulting information to the context.
  • Calls the relevent handler, the name of which is determined by appending the method name to handle_, e.g. handle_GET. The handler is passed the request, context and any positional and keyword arguments.
  • The handler will update the context and perform any required actions as necessary.
  • The handler will generally return a HttpResponse subclass directly, or call the render() method on BaseView.
  • In the latter case, render() will determine which format to serialize to using a format query parameter or content negotiation, and despatch to a format-specific rendering method (e.g. render_html()).
  • The renderer will return a HttpResponse object, which will then be passed back up the callstack to Django to be sent back to the client.

Class-based views and metaclasses

We’re using class-based views, a concept that doesn’t seem to have much of a presence on the Internet. The usual approach is to define a method __call__(self, request, …) on a class, an instance of which is then placed in an urlconf. Our approach is the following:

  • Have a base view called, oddly enough, BaseView.
  • Define a method __new__(cls, request, …) that despatches to other class methods depending on the HTTP method.
  • The __new__ method also calls a method to add common context for each of the handlers.
  • We use a metaclass to save having to put @classmethod decorators in front of every method.
  • We never create instances of the view classes; instead, __new__ returns an HttpResponse object and the class itself is place in the urlconf.

Here’s the code:

from inspect import isfunction
from django.template import RequestContext

class ViewMetaclass(type):
     def __new__(cls, name, bases, dict):
         # Wrap all functions but __new__ in a classmethod before
         # constructing the class
         for key, value in dict.items():
             if isfunction(value) and key != '__new__':
                 dict[key] = classmethod(value)
         return type.__new__(cls, name, bases, dict)

class BaseView(object):
    __metaclass__ = ViewMetaclass

    def method_not_acceptable(cls, request):
        """
        Returns a simple 405 response.
        """

        response = HttpResponse(
            'You can't perform a %s request against this resource.' %
                request.method.upper(),
            status=405,
        )
        return response

        # We could go on defining error status handlers, but there's
        # little need. These can also be overridden in subclasses if
        # necessary.

        def initial_context(cls, request, *args, **kwargs):
            """
            Returns common context for each of the HTTP method
            handlers. You will probably want to override this in
            subclasses.
            """

            return {}

        def __new__(cls, request, *args, **kwargs):
            """
            Takes a request and arguments from the URL despatcher,
            returning an HttpResponse object.
            """

            method_name = 'handle_%s' % request.method
            if hasattr(cls, method_name):
                # Construct the initial context to pass to the HTTP
                # handler
                context = RequestContext(request)
                context.update(cls.initial_context(request,
                                                   *args, **kwargs))

                # getattr returns a staticmethod , which we pass the
                # request and initial context
                handler_method = getattr(cls, method_name)
                return handler_method(request, context,
                                      *args, **kwargs)
            else:
                # Our view doesn't want to handle this method; return
                # a 405
                return cls.method_not_acceptable(request)

Our actual view code can then look a little something like this (minus all the faff with input validation and authentication):

class CheeseView(BaseView):
    def initial_context(cls, request, slug):
        return {
            'cheese': get_object_or_404(Cheese, slug=slug)
        }

    def handle_GET(cls, request, context, slug):
        return render_to_response('cheese_detail.html', context)

    def handle_DELETE(cls, request, context, slug):
        context['cheese'].delete()
        # Return a 204 No Content response to acknowledge the cheese
        # has gone.
        return HttpResponse('', status=204)

    def handle_POST(cls, request, context, slug):
        # Allow a user to change the smelliness of the cheese
        context['cheese'].smelliness = request.POST['smelliness']
        context['cheese'].save()
        return HttpResponse('', status=204)

For those who aren’t familiar with metaclasses, I’ll give a brief description of class creation in Python. First, the class statement executes all the code in the class body, using the newly bound objects (mostly the methods) to populate a dictionary. This dictionary is then passed to the __new__ method on the metaclass, along with the name of the class and its base classes. Unless otherwise specified, the metaclass will be type, but the __metaclass__ attribute is used to override this. The __new__ method can alter the name, base classes and attribute dictionary as it sees fit. In our case we are wrapping the functions in class method constructors so that they do not become instance methods.

Other things we could do are:

  • Override handle_DELETE in a subclass to return a 403 Forbidden if the cheese is important (calling super(cls, cls).handle_DELETE if it isn’t)
  • Despatch to other methods from a handler to keep our code looking modular and tidy
  • Subclass __new__ to add more parameters to the handlers on subclasses

As an example of the last point, we have an OAuthView that ensures an access token for a service and adds an urllib2 opener to the parameters which contains the necessary credentials to access a remote resource.

The subclassing view can then simply call opener.open(url) without having to worry about achieving the requisite authorisation.

Using class-based views allows us to define other methods on the views to return metadata about the resource being requested. As an example, we have a method that constructs the content for the breadcrumb trail, and another that returns the metadata for displaying in search results.

Achieving such extensibility with function-based views would be nigh on impossible.