The issue of how to propagate some errors in our Django web app
Much of what our Django application to handle (Unix) account requests does is only available to special people such as professors, who can actually approve account requests instead of just making them. Following our usual, we protect the management section of the web app with Apache HTTP Basic Authentication, where only people in designated Unix groups (such as the 'sponsors' group) have access. However, the Django application also has a 'users' table, and in order to have access to the application you have to be in it (as well as be in a Unix group that's allowed access). Normally we maintain these two things in sync; when we add someone to the relevant Unix group, we also add them as a user (and set the type of user they are, and set up other necessary data for people who can sponsor accounts). But sometimes we overlook this extra step so people wind up permitted by Apache but not in the 'users' table. If they actually try to use our web application, this causes it to stop with an error.
(This 'users' table is part of the application's own set of tables and models, not the normal Django authentication system's User table. Possibly I should have handled this differently so that it's more integrated with normal Django stuff, but when I started this web application I was new to Django and keeping things completely separate was much easier.)
Right now, a bunch of our views look like this at the start:
def approve(request): urec = get_user(request) if urec.is_staff(): ... ...
You'll note that there's no error handling. This is because
get_user()
does the brute force simple thing (with some error
checks removed):
def get_user(request): user = request.META['REMOTE_USER'] try: return models.User.objects.get(login=user) except models.User.DoesNotExist: ... log a message ... raise django.http.Http404
This is simple and reliable, but it has a downside, which is that people who run into this mistake of ours get the same HTTP 404 error page that they'd get if they were trying to go to a URL that genuinely didn't exist in our application. This is at best uninformative and at worst confusing, and I'd like to do better. Unfortunately I'm not sure what the best way to do it is.
My first attempt was to raise Django's Http404 error with a specific message string and then try to make our template for the application's 404 error page check for that string and generate a different set of messages. That failed, because as far as I can see either Django drops the message string at some point in its processing or doesn't pass it to your custom template as a template variable.
I can see three alternate approaches, none of which I'm persuaded
by. The simple but unappealing option is to change get_user()
to return an error in this situation. This would require a boilerplate
change at every place it's called to check the error and handle it
by generating a standard 'we screwed up' response page, which makes
the code feel like Go instead of Python. But at least how things
worked would be obvious (and if I returned None
, I could make
failures to handle this case relatively obvious).
The more complicated but less code approach is to raise a custom
error and wrap every function that calls get_user()
with a
decorator that catches the error to generate and return the standard
explanation page. I would have to decorate every view function that
directly or indirectly calls get_user()
(and remember to add
this if I added new functions), and decorators are sort of advanced
Python magic that aren't necessarily either clear or straightforward
for people to follow.
I suspect that the way I'm supposed to do this in Django is with
some form of middleware. If I kept to much of the current approach,
I could do this with a middleware that just used
process_exception()
,
but that doesn't seem the most idiomatic way. Since this is a common
processing step for anything protected behind HTTP Basic Authentication,
it feels like the middleware should do the user lookup itself and
attach it to the request somehow, with the actual views not even
calling get_user()
. But I don't know how to attach arbitrary
data to Django's request objects, and anyway that involves even
more Django magic than middleware that catches a custom exception
(and the magic is less clear, since the view functions would just
access data without any obvious reason for it to be there).
(I care about the amount of magic involved in any solution because my co-workers aren't particularly familiar with Django and even I only touch the code every once in a while. Possibly this means I should just use the explicit error checking version, even if it makes me twitch.)
|
|