Preconditions¶
When writing views, a common situation is that you have some checks that need to be done at the beginning of a view before the main logic, and several views might share the same checks. If the check fails you might want to redirect the user to a different page, but other options are possible, such as displaying a message (perhaps using the messages framework).
Python “decorators” are a perfect match for these kind of things.
If you haven’t used decorators at all before, I’d recommend this Primer on Python Decorators. If you just want to apply an existing decorator to a view, that’s very easy, but a good understanding of what is going on is necessary if you want to be able to implement them. Plus, you’ll get a huge amount of benefit in other ways from this very general Python technique.
First let’s look at our starting point. We have a page that should only be accessible to ‘premium’ users. If, somehow, a non-premium user gets the link to the page, they should be redirected to their account page, and also shown a message.
It might look like this:
def my_premium_page(request):
if not request.user.is_premium:
messages.info(request, "You need a premium account to access that page.")
return HttpResponseRedirect(reverse('account'))
return TemplateResponse(request, 'premium_page.html', {})
Now, we want to re-use those first 3 lines of logic. The neatest way is to put them in a decorator, which we will use like this:
@premium_required
def my_premium_page(request):
return TemplateResponse(request, 'premium_page.html', {})
To understand how to implement a decorator, it’s often useful to remember what
decorator syntax is doing. The long-hand way of defining my_premium_page
,
equivalent to the above, is like this:
def my_premium_page(request):
return TemplateResponse(request, 'premium_page.html', {})
my_premium_page = premium_required(my_premium_page)
In other words, premium_required
is a function that takes a view function as
input, and returns a new, replacement view function as output. The view function
it returns will wrap the original view function. In our case, it will also
add some additional checks and logic, and in some cases (where the user is not a
premium user), it will decide to bypass the original view function and return
its own response.
So the implementation of premium_required
will look like this:
import functools
def premium_required(view_func):
@functools.wraps(view_func)
def wrapper(request, *args, **kwargs):
if not request.user.is_premium:
messages.info(request, "You need a premium account to access that page.")
return HttpResponseRedirect(reverse('account'))
return view_func(request, *args, **kwargs)
return wrapper
The @functools.wraps(view_func)
line may not be strictly necessary. But it
makes our wrapper function view behave more nicely — for example, it copies the
name and docstring of the original view over, along with other attributes. These
make debugging nicer, and sometimes it can be important for functionality too
(for instance, if you are wrapping something that has been wrapped in
csrf_exempt
) — so you should always add it.
So far, the views we’re using it on only take a single request
, so making
our wrapper take *args
and **kwargs
might not seem necessary. But we
want this decorator to be generic and future proof, so we put those in there
from the start.
Adding multiple decorators¶
Our decorator as above has an issue — if an anonymous user accesses it,
request.user
will be an AnonymousUser
instance, and won’t have an
is_premium
attribute, which will result in a 500 error.
A nice way to tackle this is to use the Django-provided login_required
decorator, which will redirect to the login page for anonymous users.
We simply need to apply both decorators. The correct order is as
follows:
from django.contrib.auth.decorators import login_required
@login_required
@premium_required
def my_premium_page(request):
return TemplateResponse(request, 'premium_page.html', {})
The checks that login_required
does ensure that by the time we get into the
premium_required
view wrapper, we are guaranteed to have a logged in user.
Ordering multiple decorators¶
When dealing with multiple decorators, as above, ordering can be very important, and it’s easy to get confused about what order everything is happening.
The best analogy I know of is to think of it as an onion. In the centre, you have the actual view function, and each decorator adds a layer. Let’s write it out the long hand way as a visualisation:
def my_premium_page(request):
return TemplateResponse(request, 'premium_page.html', {})
my_premium_page = \
login_required(
premium_required(
my_premium_page
)
)
So, premium_required
is the innermost decorator. It is the first to be
applied to my_premium_page
, while login_required
is the
outermost decorator, and it is the last to be applied.
BUT! The decorators themselves (the functions premium_required
and
login_required
) are distinct from the wrappers they return!
So, the preconditions that the login_required
wrapper adds are run first
(because it is the outermost), and the preconditions that the
premium_required
wrapper adds are run last (because it is the
innermost).
The result is actually very intuitive — the preconditions added by each decorator are run in the order that the decorators appear in your source code.
However, you might also want to do post-processing in your view wrappers. If you do that, remember the onion metaphor — post-processing from the innermost wrapper will run before post-processing from the outermost wrapper.
Exercise¶
If the above left you horribly confused, I think the best way to understand this is to get your hands dirty with some examples, so here is an exercise.
Suppose we have the following decorators (which do nothing other than print some things):
def decorator_1(view_func):
print("In decorator_1")
def wrapper(request, *args, **kwargs):
print("In decorator_1 wrapper, pre-processing")
response = view_func(request, *args, **kwargs)
print("In decorator_1 wrapper, post-processing")
return response
return wrapper
def decorator_2(view_func):
print("In decorator_2")
def wrapper(request, *args, **kwargs):
print("In decorator_2 wrapper, pre-processing")
response = view_func(request, *args, **kwargs)
print("In decorator_2 wrapper, post-processing")
return response
return wrapper
Then, what will the following code blocks print?
>>> @decorator_1
... @decorator_2
... def my_view(request):
... print("In my_view")
... return "I am a response"
>>> response = my_view(None)
First make your guesses, then try the code from a Python prompt. If you get it wrong, have another look until you understand exactly what is going on.
Hints:
Replace the
@
syntax with the long-hand versionSimplify using no decorators, then one decorator, then two decorators
Combining multiple decorators¶
If you have multiple decorators that need to be applied in a certain order, or where you often have them together, you should probably be thinking about building a single decorator that combines them — for which I can do no better than point you to Adam Johnson’s post How to Combine Two Python Decorators!
You could also see this Stackoverflow post with general code for composing any number of decorators
Built-in decorators¶
Also, don’t miss out on the decorators and “decorator factories” than come with
Django and cover many of the common cases, such as login_required
(already
used), user_passes_test
and permission_required
Next up: Applying policies.
Discussion: Mixins do not compose¶
Django also provides mixins for applying preconditions, like LoginRequired
etc., which work by overriding the dispatch()
method.
Now, suppose we were to go the CBV route, and have a PremiumRequired
mixin
instead of @premium_required
. Let’s also add another similar check —
GoodReputationRequired
which does some kind of reputation check (perhaps
this is a social site with moderation in place). To require a user to have both,
is it enough to just add both mixins? Similarly, could I produce a new mixin
like this?
class PremiumAndGoodReputationRequired(PremiumRequired, GoodReputationRequired):
pass
The answer is: it depends.
Let’s suppose we used the UserPassesTestMixin that Django provides, which will make our mixins quite short and simple. In this case, our mixins will not compose as required, but will silently fail — only one of the checks will run. If this was a feature critical for security, that could be rather bad!
But if we implemented our base mixins some other way (like only overriding
dispatch()
, and using super()
correctly), they should compose.
(See the preconditions discussion_views.py file for a full example of both)
This issue is noted in the docs for UserPassesTestMixin
— you cannot stack
multiple uses of it.
However, docs or not, this is a great example of how, in general, mixins do not compose. I’ve said it before but it is worth repeating. You can have two mixins that work perfectly apart, but fail together. To be sure that they do compose, you have to know the implementation. Further, it’s entirely possible that when you first put them together there is no issue, but a later change means they may start failing in the worst possible way — silently.
This is a horrible way to write software! As much as possible, we should choose techniques where you can simply depend on the interface of some code to know that you are using it correctly, rather than its implementation.
Decorators aren’t prone to this problem. Mixins are like injecting things right into the middle of someone else’s code, hoping that the context will fit, while decorators wrap existing functionality depending only on its interface.