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

1from typing import Any 

2 

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 _ 

8 

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 

12 

13# from .forms.role_forms import RoleSelectForm 

14from mesh.model.user.user_interfaces import ImpersonateData 

15 

16ROLE_SWITCH_QUERY_PARAM = "_role_switched" 

17 

18 

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. 

23 

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

28 

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

38 

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 ) 

43 

44 self.request = request 

45 

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) 

49 

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 

56 

57 # The obtained role_handler might not be fully initialized. 

58 self.role_handler.complete_init() 

59 

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

66 

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

68 

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 

77 

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 

82 

83 return super().dispatch(request, *args, **kwargs) # type:ignore 

84 

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

89 

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

91 redirecting to home. 

92 """ 

93 return False 

94 

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

99 

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 

115 

116 def get_fail_redirect_uri(self) -> str: 

117 return self.fail_redirect_uri 

118 

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

127 

128 context["role_handler"] = self.role_handler 

129 

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 

134 

135 return context 

136 

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 ) 

143 

144 

145class RoleMixin(LoginRequiredMixin, BaseRoleMixin): 

146 """ 

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

148 

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

150 """ 

151 

152 pass