# coding=utf-8 import logging from django.shortcuts import redirect from django.contrib import messages from web.models import Worker, WorkerStudyRole from web.models.constants import GLOBAL_STUDY_ID from django.http import HttpResponseForbidden from django.contrib.auth.models import Permission import functools logger = logging.getLogger(__name__) ''' Dear developer reader, welcome to one of the most precious pieces of magic python can deliver. This cute class decorator provides the same functionality as standard decorator functions, but with the additional ability to keep a state along the decorated function calls. The decorator is used as follows: @PermissionDecorator('change_worker') #codename of the permission def worker_edit(request, worker_id): ... In this case, this decorator class has two static variables, codenames and n_decorators. The main question this decorator intends to answer is: - Could I get all the codenames from all the decorators used along the code? To do so, we have a static variable, codenames, and when an instance of the decorator is created, the permission codename used in the decorator is stored. - Then, in the code, we could do: PermissionDecorator.codenames to access the list of codenames used in the whole code. - We also have a static set of permissions, in which we can add the Permission object obtained from the codename. This is also safe, since, if no Permission is found we can raise an exception and avoid mid-runtime errors since this would be done during the program initialization. - This solves the following problem: We wanted to have a list of the used codenames along the code, so this used codenames would be visible, while the unused ones are kept hidden. This kind of decorators could also be used to register functions, having for example, a decorator called @RegisterFunction that does not receive any argument, and hence, the __init__ method would receive the decorated function instead of the decorator arguments. Or, if the decorator has arguments, using the __call__ method to keep a static list of functions within the class. This way you could gather all the decorated functions to use them later, accessing the static variables from the decorators. This is useful to avoid hardcoded lists of functions or decorator arguments. Two good references on the topic: https://www.thecodeship.com/patterns/guide-to-python-function-decorators/ https://realpython.com/primer-on-python-decorators/#classes-as-decorators ''' class PermissionDecorator: codenames = set() permission_ids = set() permissions = None n_decorators = 0 def __init__(self, perm_codename): ''' This method is called when the code is interpreted, this is, when a function is decorated because an instance of the decorator is created. ''' PermissionDecorator.codenames.add(perm_codename) #register permission codename PermissionDecorator.n_decorators += 1 #count number of decorators query_result = Permission.objects.filter(codename=perm_codename) if len(query_result) < 0: logger.warn('Codename {} used in PermissionDecorator does not exist.'.format(perm_codename)) raise ValueError if query_result[0].id not in PermissionDecorator.permission_ids: #avoid re-query if the permissions remain the same PermissionDecorator.permission_ids.add(query_result[0].id) PermissionDecorator.permissions = Permission.objects.filter(id__in=PermissionDecorator.permission_ids).all() #variables to be used on decorator call self.perm_codename = perm_codename def __call__(self, func): ''' This method is also called when the function is decorated ''' def func_wrapper(request, **kwargs): ''' This method is called when the decorated function is called ''' if request.user.is_superuser: return func(request, **kwargs) else: worker = Worker.get_by_user(request.user) roles = WorkerStudyRole.objects.filter(worker=worker, study_id=GLOBAL_STUDY_ID) if roles.count() > 0: permissions = roles[0].permissions.filter(codename=self.perm_codename) if len(permissions) > 0: return func(request, **kwargs) messages.error(request, 'You are not authorized to view this page. Request permissions to the system administrator.') #avoid loops if the HTTP_REFERER header is set to the visited URL http_referer = request.META.get('HTTP_REFERER', 'web.views.index') if http_referer == request.build_absolute_uri() or http_referer == request.path: http_referer = 'web.views.index' return redirect(http_referer) return func_wrapper