Django has added class views in addition to the older function based ones. Traditionally tasks such as testing authorisation has been applied via decorator functions.
@login_required def my_view: return response
The recommended approach to do this for a class view is to apply a method decorator.
There is a utility convertor method_decorator, to do this for function decorators.
class ProtectedView(TemplateView): @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(ProtectedView, self).dispatch(*args, **kwargs)
However this isn't ideal since it is less easy to check all the methods for decorators than just look at the top of the view function as before.
So why not use a class decorator instead to make things more clear. Fine, except we do actually want to decorate the dispatch method. But we can add a utility decorator that wraps this up.*
@class_decorator(login_required, 'dispatch') class ProtectedView(TemplateView): def dispatch(self, *args, **kwargs): return super(ProtectedView, self).dispatch(*args, **kwargs)
But Django's generic class views contain more than just the TemplateView, they have generic list, detail and update views. All of which use a standard pattern to associate object(s) in the context data. Not only that but the request will also have user data populated if the view requires a login.
What I want to do is have a simple decorator that just takes a list of permissions, then ensures users who access the class view must login and then have each of these object permissions checked for the context data object(s). So my decorator for authorising user object permissions will be @class_permissions('view', 'edit', 'delete')
To do this the class_permissions decorator itself, is best written as a class. The class can then combine the actions of three method decorators on the two Django generic class view methods - dispatch and get_context_data.
Firstly login_required wraps dispatch - then dispatch_setuser wraps this to set the user that login_required delivers as an attribute of the class_permissions class.
These must decorate in the correct order to work.
Finally class_permissions wraps get_context_data to grab the view object(s). The user, permissions and objects can now all be used to test for object level authorisation - before a user is allowed access to the view. The core bits of the final code are below - my class decorator class is done :-)
class class_permissions(object): """ Tests the objects associated with class views against permissions list. """ perms =  user = None view = None def __init__(self, *args): self.perms = args def __call__(self, View): """ Main decorator method """ self.view = View def _wrap(request=None, *args, **kwargs): """ double decorates dispatch decorates get_context_data passing itself which has the required data """ setter = getattr(View, 'dispatch', None) if setter: decorated = method_decorator( dispatch_setuser(self))(setter) setattr(View, setter.__name__, method_decorator(login_required)(decorated)) getter = getattr(View, 'get_context_data', None) if getter: setattr(View, getter.__name__, method_decorator( decklass_permissions(self))(getter)) return View return _wrap()
The function decorators and imports that are used by the decorator class above
from functools import wraps from django.utils.decorators import method_decorator def decklass_permissions(decklass): """ The core decorator that checks permissions """ def decorator(view_func): """ Wraps get_context_data on generic view classes """ @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(**kwargs): """ Gets objects from get_context_data and runs check """ context = view_func(**kwargs) obj_list = context.get('object_list', ) if not obj_list: obj = context.get('subobject', context.get('object', None)) if obj: obj_list = [obj, ] check_permissions(decklass.perms, decklass.user, obj_list) return context return _wrapped_view return decorator def dispatch_setuser(decklass): """ Decorate dispatch to add user to decorator class """ def decorator(view_func): @wraps(view_func, assigned=available_attrs(view_func)) def _wrapped_view(request, *args, **kwargs): if request: decklass.user = request.user return view_func(request, *args, **kwargs) return _wrapped_view return decorator
Although all this works fine, it does seem overly burdened with syntactic sugar. I imagine there may be a more concise way to achieve the results I want. If anyone can think of one, please comment below.
* I didnt show the code for class_decorator since it is just a simplified version of the class_permissions example above.