Coverage for src / mesh / views / mixins.py: 85%
75 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-09 13:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-09 13:14 +0000
1from abc import ABC
2from typing import Any
4from django.contrib import messages
5from django.contrib.auth.mixins import LoginRequiredMixin
6from django.http import Http404, HttpRequest, HttpResponseRedirect
7from django.urls import reverse_lazy
8from django.utils.translation import gettext_lazy as _
9from ptf.views.components.breadcrumb import Breadcrumb
11from mesh.models.roles.base_role import Role
12from mesh.models.roles.role_handler import ROLE_HANDLER_CACHE, RoleHandler
13from mesh.models.user.user_interfaces import ImpersonateData
15ROLE_SWITCH_QUERY_PARAM = "_role_switched"
18class BaseRoleMixin:
19 """
20 Mixin responsible for instantiating a RoleHandler for the current view.
21 It should not been used directly. Use the `RoleMixin` instead.
23 Additionally, it contains the logic involving navigation and role restriction:
24 - force the user role if the route is role protected.
25 - restrict dispatch on hookable conditions (cf. `RoleMixin.restrict_dispatch`).
26 """
28 fail_redirect_uri = reverse_lazy("mesh:home")
29 request: HttpRequest
30 access_restricted_message = _(
31 "Your are not allowed to access the requested content with your current rights."
32 )
33 message_on_restrict = True
34 # Used to restrict the view to users with at least one active role in the list.
35 restricted_roles: list[type[Role]] = []
37 def dispatch(self, request: HttpRequest, *args, **kwargs):
38 assert request.user.is_authenticated, (
39 "ERROR: `BaseRoleMixin` should not be used directly. Use `RoleMixin` instead."
40 )
42 self.request = request
44 # The `role_handler` object might have already been derived by a middleware.
45 # See `TokenBackend` for more info.
46 role_handler_cache = getattr(request, ROLE_HANDLER_CACHE, None)
48 if role_handler_cache is None or role_handler_cache.user.pk != request.user.pk:
49 self.role_handler = RoleHandler(request.user, request)
50 else:
51 self.role_handler = role_handler_cache
52 if self.role_handler.request is None:
53 self.role_handler.request = request
55 # The obtained role_handler might not be fully initialized.
56 self.role_handler.complete_init()
58 # Force the user role if the view is role-restricted.
59 # /!\ Do not force role if we're performing a role switch as it could be re-changed
60 # instantaneously.
61 role_switch = request.GET.get(ROLE_SWITCH_QUERY_PARAM, None) == "true"
62 if not role_switch:
63 self.force_role()
65 self.request.user.impersonate_data = ImpersonateData.from_session(self.request.session)
67 if self.restrict_dispatch(request, *args, **kwargs):
68 # If the role the user is switching to is not allowed access, simply
69 # redirect to the redirect uri without adding a message
70 # By default, the role switch redirects to the same page but the new role
71 # might not have the required rights to access.
72 response = HttpResponseRedirect(self.get_fail_redirect_uri())
73 if role_switch:
74 return response
76 # Otherwise the user tried to access unallowed URL.
77 if self.message_on_restrict:
78 messages.warning(request, self.get_access_restricted_message())
79 return response
81 return super().dispatch(request, *args, **kwargs)
83 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
84 """
85 Custom hook to restrict access to the current view. \\
86 When `True`, redirects to `self.get_fail_redirect_uri()`.
88 TBD: Maybe we should raise a HTTP 404 error when access is restricted instead of
89 redirecting to home.
90 """
91 return False
93 def force_role(self) -> None:
94 """
95 Switch the current user's role to the first active role matching the view's
96 `restricted_roles`.
98 Raise an 404 error if there's no matching active role.
99 """
100 if not self.restricted_roles:
101 return
102 if self.request.current_role.__class__ in self.restricted_roles:
103 return
105 # Use of RoleHandler for role switch
106 role_handler = RoleHandler(self.request.user, self.request)
107 for role_class in self.restricted_roles:
108 try:
109 new_role = role_handler.switch_role(role_class.code())
110 # Update request current_role for the view (usually a middleware job)
111 self.request.current_role = new_role
112 return
113 except Exception:
114 continue
115 raise Http404
117 def get_fail_redirect_uri(self) -> str:
118 return self.fail_redirect_uri
120 def get_context_data(self, *args, **kwargs) -> dict[str, Any]:
121 """
122 Overloads the default context data with the request params.
123 """
124 if hasattr(super(), "get_context_data"):
125 context = super().get_context_data(*args, **kwargs)
126 else:
127 context = {}
129 context["current_role"] = self.request.current_role
130 context["available_roles"] = self.request.available_roles
132 return context
134 def get_access_restricted_message(self) -> str:
135 return (
136 self.access_restricted_message
137 + "<br>"
138 + f"Current role: {self.request.current_role.name()}"
139 )
142class RoleMixin(LoginRequiredMixin, BaseRoleMixin):
143 """
144 Mixin to be used when handling role-related content.
146 BaseRoleMixin is irrelevant if the user is not logged in.
147 """
149 pass
152class BreadcrumbMixin(ABC):
153 breadcrumb: Breadcrumb
155 def get_context_data(self, **kwargs):
156 context = super().get_context_data(**kwargs)
157 context["breadcrumb"] = self.breadcrumb
158 return context