I've written in the past about my dislike for Django's Class Based Views. Django's CBVs add a lot of complexity and verbosity, and simply get in the way of some moderately common patterns (e.g. when you have two forms in a single view). It seems I'm not alone as a Django core dev who thinks that way.
In this post, however, I'll write about a different approach that I took in one project, which can be summed up like this:
Write your own base class.
For really simple model views, Django's own CBVs can be a time saver. For anything more complex, you will run into difficulties, and will need some heavy documentation at the very least.
One solution is to use a simplified re-implementation of Class Based Views. My own approach is to go even further and start from nothing, writing your own base class, while borrowing the best ideas and incorporating only what you need.
Steal the good ideas
The as_view
method provided by the Django's View
class is a great idea – while it may
not be obvious, it was hammered out after a lot of discussion as a way to help
promote request isolation by creating a new instance of the class to handle
every new request. So I'll happily steal that!
Reject the bad
Personally I dislike the dispatch
method with its assumption that handling
of GET
and POST
is going to be completely different, when often they can
overlap a lot (especially for typical form handling). It has even introduced
bugs for me where a view rejected POST requests, when what it needed to do was
just ignore the POST data, which required extra code!
So I replaced that with a simple handle
function that you have to implement
to do any logic.
I also don't like the way that template names are automatically built from model names etc. – this is convention over configuration, and it makes life unnecessarily hard for a maintenance programmer who greps to find out where a template is used. If that kind of logic is used, you just Have To Know where to look to see if a template is used at all and how it is used. So that is going.
Flatten the stack
A relatively flat set of base classes is going to be far easier to manage than a large set of mixins and base classes. By using a flat stack, I can avoid writing crazy hacks to subvert what I have inherited.
Write the API you want
For instance, one of the things I really dislike about Django's CBVs is the extremely verbose way of adding new data to the context, which is something that ought to be really easy, but instead requires 4 lines:
class MyView(ParentView): def get_context_data(self, **kwargs): context = super(MyView, self).get_context_data(**kwargs) context['title'] = "My title" # This is the only line I want to write! return context
In fact, it is often worse, because the data to add to the context may actually
have been calculated in a different method, and stuck on self
so that
get_context_data
could find it. And you also have the problem that it is
easy to do it wrong e.g. if you forget the call to super
things start breaking in
non-obvious ways.
(In searching GitHub for examples, I actually found hundreds and hundreds of examples that look like this:
class HomeView(TemplateView): # ... def get_context_data(self): context = super(HomeView, self).get_context_data() return context
This doesn't make much sense, until I realised that people are using boilerplate generators/snippets to create new CBVs – such as this for emacs and this for vim, and this for Sublime Text. You know when you have created an unwieldy API when people need these kinds of shortcuts.)
So, the answer is:
Imagine the API you want, then implement it.
This is what I would like to write for static additions to the context:
and for dynamic:
class MyView(ParentView): def context(self): return {'things': Thing.objects.all() if self.request.user.is_authenticated() else Thing.objects.public()} # Or perhaps using a lambda: context = lambda self: ...
And I would like any context defined by ParentView
to be automatically
accumulated, even though I didn't explicitly call super
. (After all, you
almost always want to add to context data, and if necessary a subclass could
remove specific inherited data by setting a key to None
).
I'd also like for any method in my CBV to simply be able to add data to the context directly, perhaps by setting/updating an instance variable:
Of course, it goes without saying that this shouldn't clobber anything at the
class level and violate request isolation, and all of these methods should work
together nicely in the way you would expect. And it should be impossible to
accidentally mutate any class-defined context
dictionary from within a method.
Now, sometimes after you've finished dreaming, you find your imagined API is too
tricky to implement due to a language issue, and has to be modified. In this
case, the behaviour is easily achievable, although it is a little bit magic,
because normally defining a method in a subclass without using super
means
that the super class definition would be ignored, and for class attributes you
can't use super
at all.
So, my own preference is to make this more obvious by using the name
magic_context
for the first two (the class attribute and the method). That
way I get the benefits of the magic, while not tripping up any maintainer – if
something is called magic_foo
, most people are going to want to know why it
is magic and how it works.
The implementation
uses a few tricks, the heart of which is using
reversed(self.__class__.mro())
to get all the super-classes and their
magic_context
attributes, iteratively updating a dictionary with them.
Notice too how the TemplateView.handle method is extremely simple, and just calls out to another method to do all the work:
This means that a subclass that defines handle
to do the actual logic
doesn't need to call super
, but just calls the same method directly:
class MyView(TemplateView): template_name = "mytemplate.html" def handle(self, request): # logic here... return self.render({'some_more': 'context_data'})
In addition to these things, I have various hooks that I use to handle things like AJAX validation for form views, and RSS/Atom feeds for list views etc. Because I'm in control of the base classes, these things are simple to do.
Conclusion
I guess the core idea here is that you shouldn't be constrained by what Django has supplied. There is actually nothing about CBVs that is deeply integrated into Django, so your own implementation is just as valid as Django's, but you can make it work for you. I would encourage you to write the actual code you want to write, then make the base class that enables it to work.
The disadvantage, of course, is that maintenance programmers who have memorised the API of Django's CBVs won't benefit from that in the context of a project which uses another set of base classes. However, I think the advantages more than compensate for this.
Feel free to borrow any of the code or ideas if they are useful!