Hi all,

I'd like to describe a little of the way we're using Django for our
site, http://m.ox.ac.uk/. I'd very much like to find out whether other
peoplethink that our approach is sensible, and if it is, I thought this
post might be useful for other people in tackling some of the issues
we've come across.

We're using class-based views, a concept that doesn't seem to have much
of a presence on the Internet[0]. 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 call
   cls.method_not_acceptable 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.


Now for view validation...

As you may have noticed, all these methods (handle_foo, initial_context)
have similar signatures. To make sure they're consistent we have a test
that looks through all the installed apps looking for classes that
subclass BaseView. It then uses inspect.getargspec to compare the
signatures. The alternative to this would be to check them in the
metaclass, and raise an appropriate error at class creation time.


Hopefully you find this useful[1]. We'd be very grateful to hear any
suggestions or criticisms that you may have. I certainly don't suggest
this approach is applicable in every case, but it's helped us to adhere
as much as possible to the DRY principle.

My only concern is that with such levels of indirection and 'advanced
features', it may be a little daunting for any new developers -- I
didn't know anything about metaclasses or how method binding actually
works until yesterday.

Yours,

Alex


[0] Simon Willison seems to talk about them occasionally, though
searching Google for 'class-based views' doesn't turn up much.
[1] If all goes well, I'll make a blog post of this.

-- 
Alexander Dutton
Erewhon Project Officer | m.ox.ac.uk developer
Oxford University Computing Services, ℡ 01865 (6)13483

--

You received this message because you are subscribed to the Google Groups 
"Django users" group.
To post to this group, send email to django-us...@googlegroups.com.
To unsubscribe from this group, send email to 
django-users+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-users?hl=en.


Reply via email to