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

1from abc import ABC 

2from typing import Any 

3 

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 

10 

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 

14 

15ROLE_SWITCH_QUERY_PARAM = "_role_switched" 

16 

17 

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. 

22 

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 """ 

27 

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]] = [] 

36 

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 ) 

41 

42 self.request = request 

43 

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) 

47 

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 

54 

55 # The obtained role_handler might not be fully initialized. 

56 self.role_handler.complete_init() 

57 

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() 

64 

65 self.request.user.impersonate_data = ImpersonateData.from_session(self.request.session) 

66 

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 

75 

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 

80 

81 return super().dispatch(request, *args, **kwargs) 

82 

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()`. 

87 

88 TBD: Maybe we should raise a HTTP 404 error when access is restricted instead of 

89 redirecting to home. 

90 """ 

91 return False 

92 

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`. 

97 

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 

104 

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 

116 

117 def get_fail_redirect_uri(self) -> str: 

118 return self.fail_redirect_uri 

119 

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 = {} 

128 

129 context["current_role"] = self.request.current_role 

130 context["available_roles"] = self.request.available_roles 

131 

132 return context 

133 

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 ) 

140 

141 

142class RoleMixin(LoginRequiredMixin, BaseRoleMixin): 

143 """ 

144 Mixin to be used when handling role-related content. 

145 

146 BaseRoleMixin is irrelevant if the user is not logged in. 

147 """ 

148 

149 pass 

150 

151 

152class BreadcrumbMixin(ABC): 

153 breadcrumb: Breadcrumb 

154 

155 def get_context_data(self, **kwargs): 

156 context = super().get_context_data(**kwargs) 

157 context["breadcrumb"] = self.breadcrumb 

158 return context