Coverage for src/mesh/views/mixins.py: 85%
66 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
1from typing import Any
3from django.contrib import messages
4from django.contrib.auth.mixins import LoginRequiredMixin
5from django.http import Http404, HttpRequest, HttpResponseRedirect
6from django.urls import reverse_lazy
7from django.utils.translation import gettext_lazy as _
9from mesh.model.exceptions import RoleException
10from mesh.model.roles.base_role import Role
11from mesh.model.roles.role_handler import ROLE_HANDLER_CACHE, RoleHandler
13# from .forms.role_forms import RoleSelectForm
14from mesh.model.user.user_interfaces import ImpersonateData
16ROLE_SWITCH_QUERY_PARAM = "_role_switched"
19class BaseRoleMixin:
20 """
21 Mixin responsible for instantiating a RoleHandler for the current view.
22 It should not been used directly. Use the `RoleMixin` instead.
24 Additionally, it contains the logic involving navigation and role restriction:
25 - force the user role if the route is role protected.
26 - restrict dispatch on hookable conditions (cf. `RoleMixin.restrict_dispatch`).
27 """
29 role_handler: RoleHandler
30 fail_redirect_uri = reverse_lazy("mesh:home")
31 request: HttpRequest
32 access_restricted_message = _(
33 "Your are not allowed to access the requested content with your current rights."
34 )
35 message_on_restrict = True
36 # Used to restrict the view to users with at least one active role in the list.
37 restricted_roles: list[type[Role]] = []
39 def dispatch(self, request: HttpRequest, *args, **kwargs):
40 assert request.user.is_authenticated, (
41 "ERROR: `BaseRoleMixin` should not be used directly. Use `RoleMixin` instead."
42 )
44 self.request = request
46 # The `role_handler` object might have already been derived by a middleware.
47 # See `TokenBackend` for more info.
48 role_handler_cache = getattr(request, ROLE_HANDLER_CACHE, None)
50 if role_handler_cache is None or role_handler_cache.user.pk != request.user.pk:
51 self.role_handler = RoleHandler(request.user, request) # type:ignore
52 else:
53 self.role_handler: RoleHandler = role_handler_cache
54 if self.role_handler.request is None: 54 ↛ 58line 54 didn't jump to line 58 because the condition on line 54 was always true
55 self.role_handler.request = request
57 # The obtained role_handler might not be fully initialized.
58 self.role_handler.complete_init()
60 # Force the user role if the view is role-restricted.
61 # /!\ Do not force role if we're performing a role switch as it could be re-changed
62 # instantaneously.
63 role_switch = request.GET.get(ROLE_SWITCH_QUERY_PARAM, None) == "true"
64 if not role_switch:
65 self.force_role()
67 self.request.user.impersonate_data = ImpersonateData.from_session(self.request.session)
69 if self.restrict_dispatch(request, *args, **kwargs):
70 # If the role the user is switching to is not allowed access, simply
71 # redirect to the redirect uri without adding a message
72 # By default, the role switch redirects to the same page but the new role
73 # might not have the required rights to access.
74 response = HttpResponseRedirect(self.get_fail_redirect_uri())
75 if role_switch: 75 ↛ 76line 75 didn't jump to line 76 because the condition on line 75 was never true
76 return response
78 # Otherwise the user tried to access unallowed URL.
79 if self.message_on_restrict: 79 ↛ 81line 79 didn't jump to line 81 because the condition on line 79 was always true
80 messages.warning(request, self.get_access_restricted_message())
81 return response
83 return super().dispatch(request, *args, **kwargs) # type:ignore
85 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
86 """
87 Custom hook to restrict access to the current view. \\
88 When `True`, redirects to `self.get_fail_redirect_uri()`.
90 TBD: Maybe we should raise a HTTP 404 error when access is restricted instead of
91 redirecting to home.
92 """
93 return False
95 def force_role(self) -> None:
96 """
97 Switch the current user's role to the first active role matching the view's
98 `restricted_roles`.
100 Raise an 404 error if there's no matching active role.
101 """
102 if (
103 self.restricted_roles
104 and self.role_handler.current_role.__class__ not in self.restricted_roles
105 ):
106 for role in self.restricted_roles:
107 try:
108 self.role_handler.switch_role(role.code())
109 except RoleException:
110 continue
111 else:
112 return
113 raise Http404
114 return
116 def get_fail_redirect_uri(self) -> str:
117 return self.fail_redirect_uri
119 def get_context_data(self, *args, **kwargs) -> dict[str, Any]:
120 """
121 Overloads the default context data with the role handler object.
122 """
123 if hasattr(super(), "get_context_data"):
124 context = super().get_context_data(*args, **kwargs) # type:ignore
125 else:
126 context = {}
128 context["role_handler"] = self.role_handler
130 active_roles = self.role_handler.get_active_roles()
131 # choices = [(role.code(), role.name()) for role in active_roles]
132 # initials = {"role_code": choices}
133 context["available_roles"] = active_roles
135 return context
137 def get_access_restricted_message(self) -> str:
138 return (
139 self.access_restricted_message
140 + "<br>"
141 + f"Current role: {self.role_handler.current_role.name()}"
142 )
145class RoleMixin(LoginRequiredMixin, BaseRoleMixin):
146 """
147 Mixin to be used when handling role-related content.
149 BaseRoleMixin is irrelevant if the user is not logged in.
150 """
152 pass