Django class-based views have been getting quite a bit of attention recently. I'm not going to add my voice to the debate, but one thing that can be said is that the documentation for class-based (generic) views is not on a par with what we've come to expect from the Django project. This was highlighted by a Stack Overflow question I answered recently.
I enjoyed writing the previous series on Django in Production, and fitting this all into one post would be somewhat of a marathon, so this marks the start of another.
For the next couple of posts, I'm going to share some of the mixins and other labour-saving tricks I've come across whilst using class-based views in my day-to-day work. Who knows, it might serve as a contribution to the community as we start to figure out the best way of using these new fangled things, but at the very least I hope it will be useful to people like Vlad.
One of the issues I came up against almost immediately was restricting views to authenticated users. With functional views, the code looks something like this:
@login_required def secret_view(request, *args, **kwargs): return HttpResponse('The answer is 42')
However, with class-based views, you're forced into writing the following:
class SecretView(View): @method_decorator(login_required) def dispatch(self, request, *args, **kwargs): return super(SecretView, self).dispatch(request, *args, **kwargs) def get(self, request, *args, **kwargs): return HttpResponse('The answer is 42')
(if, like me, you dislike including this kind of logic in the URLconf)
Once you have more than a couple of views (one, strictly) that require this
views.py starts to look very wet indeed!
Before settling on the idea of mixins, my first attempt involved the use of
a "parent" view for the project, inheriting from
django.views.generic.base.View, which would contain the decorated version
dispatch() and anything else that might be required.
I quickly learned that this technique wasn't ideal, though, when I started
getting an unfamiliar exception:
TypeError: MRO conflict among bases.
I still feel unqualified to explain exactly how and why this error occurs, but
at the very least it has forced me to (somewhat) understand Python's
Method Resolution Order.
In any case, a neater and more Pythonic (Djangonic?) solution to this problem
is to use mixins. In fact, django-braces provides a number of
them for this very purpose, including
PermissionRequiredMixin. A mixin which restricts a view to only logged in
users might look something like this:
This might seem like a really simple class (in truth, it is), but it hinges on
super works. You may have noticed that, because this class inherits
object, it's parent class will have no
It's out of the scope of this post to discuss the intricacies, but I would strongly recommend reading this article to understand them. The important thing to remember when using a mixin like this - which overrides the implementation of a method defined in a sibling class - is to put it first in the list of base classes:
class SecretView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): return HttpResponse('The answer is 42')
I've used a few mixins similar to this, for restricting views based on
is_superuser, and best of all, the user's permissions:
This mixin, based on the
django-braces, allows a set of permissions to be required for
a particular view. Importantly, the
dispatch() method checks these
permissions before the view is executed. If it checked after,
a data-altering operation could be executed even though the user who requested
it was not authorized to perform that operation (although they wouldn't get to
see the result).
Using this mixin to control access is easy:
class PrivateView(PermissionsRequiredMixin, View): required_permissions = ( 'app.view_private', ) def get(self, request, *args, **kwargs): return HttpResponse('Permission granted!')
To see all these examples and more, check out the gist.
In the next post of this series, I'll be sharing some mixins and template tags which make filtering and sorting a breeze.