views:

142

answers:

2

I have a dozen or so permission lookups on views that make sure users have the right permissions to do something on the system (ie make sure they're in the right group, if they can edit their profile, if they're group administrators, etc).

A check might look like this:

from django.contrib.auth.decorators import user_passes_test

test_canvote = lambda u: u.has_perm('polls.can_vote')

@user_passes_test(test_canvote)
def my_view(request):
    # ...

This is actually code from the Django tutorial (mine is a little uglier). Sometimes a check is very database intensive, firing off multiple queries. With lots of users hitting permission-checked pages, things can quickly get quite slow.

My question is, can I (with your help) build a wrapper (or replacement) for the user_passes_test decorator that searches the cache for a key 'TESTCACHE' + user.pk + 'testname' and if it doesn't exist, executes the test and saves its result.

I've never written a decorator before but I imagine it would look nearly identical to the user_passes_test one, just passing the test as a string:

@cached_user_passes_test('test_canvote')
def my_view(request):
   # ...

And as ever, let me know if I'm mad or if Django already does this for me (so I've problems elsewhere).

Edit: The standard decorators can be found here: http://code.djangoproject.com/browser/django/trunk/django/contrib/auth/decorators.py

I think it might be easier replacing user_passes_test than wrapping it so here's the starting point. Of course, if you feel I'm incorrect in that statement, let me know:

try:
    from functools import update_wrapper, wraps
except ImportError:
    from django.utils.functional import update_wrapper, wraps  # Python 2.3, 2.4 fallback.

from django.contrib.auth import REDIRECT_FIELD_NAME
from django.http import HttpResponseRedirect
from django.utils.http import urlquote
from django.utils.decorators import auto_adapt_to_methods

def user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """
    if not login_url:
        from django.conf import settings
        login_url = settings.LOGIN_URL

    def decorator(view_func):
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request.user):
                return view_func(request, *args, **kwargs)
            path = urlquote(request.get_full_path())
            tup = login_url, redirect_field_name, path
            return HttpResponseRedirect('%s?%s=%s' % tup)
        return wraps(view_func)(_wrapped_view)
    return auto_adapt_to_methods(decorator)
+1  A: 

You might need to serialize the function (which I'm not doing when I use it as the key to the cache), but something like this should work:

from django.core.cache import cache

def cached_user_passes_test(test_func, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    if not login_url:
        from django.conf import settings
        login_url = settings.LOGIN_URL

    def decorator(view_func):
        def _wrapped_view(request, *args, **kwargs):
            key = str(test_func) + str(request.user)
            cached_test_result = cache.get(key)
            if cached_test_result != None:
                test_result = cached_test_result
            else:
                test_result = test_func(request.user)
                cache.set(key, test_result, 60)       

            if test_result:
                return view_func(request, *args, **kwargs)
            path = urlquote(request.get_full_path())
            tup = login_url, redirect_field_name, path
            return HttpResponseRedirect('%s?%s=%s' % tup)
        return wraps(view_func)(_wrapped_view)
    return auto_adapt_to_methods(decorator)
ara818
The arguments for the test function would also need to be part of the key too for obvious reasons.
Oli
Of course, trying to be too clever with the last one ... edited to do the getting and setting inside the decorator. Would this work?
ara818
A: 

First you can simply write:

from django.contrib.auth.decorators import permission_required

@permission_required('polls.can_vote')
def my_view(request):
    # ...

Second, if the permissions don't change in time, you are free to store some info in the session (I found it more convenient, than storing in a cache of whatever type), when the user logs in.

But remember, that if you change the permissions, the user has to log out, and back in to work with the new permissions.

pihentagy
I think you've missed the point. As I said, my tests are slightly more complex than a simple `permission_required` and all revolve around on `user_passes_test`. "How can I cache a decorator?" might have been a more accurate title. This is much more about how you do the caching.
Oli