Coverage for src / mesh / views / views_base.py: 84%

93 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-09 13:14 +0000

1from typing import Any 

2 

3from django.contrib import messages 

4from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect 

5from django.utils import timezone 

6from django.utils.translation import gettext as _ 

7from ptf.url_utils import add_query_parameters_to_url 

8 

9from mesh.models.orm.editorial_models import EditorialDecision 

10from mesh.models.orm.review_models import Review 

11from mesh.models.orm.submission_models import ( 

12 Submission, 

13 SubmissionVersion, 

14) 

15from mesh.views.forms.base_forms import ( 

16 SUBMIT_QUERY_PARAMETER, 

17 FormAction, 

18 SubmittableModelForm, 

19) 

20 

21 

22class SubmittableModelFormMixin: 

23 """ 

24 View mixin for submittable model forms. 

25 To be used together with: 

26 - RoleMixin 

27 - TemplateResponseMixin 

28 - FormMixin - The form class must inherit SubmittableModelForm 

29 

30 This uses the same view for saving the form (and model), submitting the form 

31 and confirming the submission of the form. 

32 The status is known through the booleans `_submit` and `_submit_confirm`. 

33 The `form_pre_save` and `form_post_save` hooks enable additional processing. 

34 

35 Normal workflow: 

36 1. The user saves the form any number of times (draft). 

37 2. When ready, the user clicks the "Submit" button 

38 3. The user is redirected to a recap of the form to be submitted (default: same 

39 route & view with the disabled form). 

40 4. The user confirms the submission of the form. 

41 5. The underlying model gets updated: `submitted=True` & `date_submitted=now` 

42 

43 Steps 4 and 5 have a simple implementation in this view mixin. 

44 These steps can be externalized to a distinct view. In that case, don't forget 

45 to update the model's `submitted` and `date_submitted` fields ! 

46 """ 

47 

48 # Whether the request (POST) is the submission of the form/model. 

49 _submit = False 

50 # Whether the request (GET or POST) is the confirmation of the submission 

51 # of the form/model. 

52 _submit_confirm = False 

53 # Whether to add a message when redirecting to the confirmation URL. 

54 add_confirm_message = True 

55 # Only for typing. This attribute should be set somewhere else (usually by the 

56 # RoleMixin) 

57 request: HttpRequest 

58 

59 def submit_url(self) -> str: 

60 """ 

61 URL to redirect to to when the user submits the form. 

62 

63 The default is the current URL with an added query parameter indicating 

64 the confirmation request. 

65 """ 

66 return add_query_parameters_to_url( 

67 self.request.build_absolute_uri(), {SUBMIT_QUERY_PARAMETER: ["true"]} 

68 ) 

69 

70 def form_pre_save(self, form: SubmittableModelForm) -> None: 

71 """ 

72 Hook called before `form_valid` (wich saves the underlying model to DB) 

73 when posting the form. 

74 """ 

75 pass 

76 

77 def form_post_save( 

78 self, form: SubmittableModelForm, original_response: HttpResponse 

79 ) -> HttpResponse: 

80 """ 

81 Hook called after `form_valid` (wich saves the underlying model to DB) 

82 when posting the form. 

83 

84 This must return an HTTP response. 

85 """ 

86 return original_response 

87 

88 def form_valid(self, form): 

89 """ 

90 Checks whether the POST request is a simple save, a submit or 

91 a submit confirmation. 

92 """ 

93 if FormAction.SUBMIT.value in self.request.POST: 

94 self._submit = True 

95 elif ( 

96 FormAction.SUBMIT_CONFIRM.value in self.request.POST 

97 and form.instance.submitted is False 

98 ): 

99 self._submit_confirm = True 

100 form.instance.submitted = True 

101 if form.instance.date_submitted is None: 

102 form.instance.date_submitted = timezone.now() 

103 

104 self.form_pre_save(form) 

105 

106 resp = super().form_valid(form) 

107 

108 if self._submit: 

109 resp = HttpResponseRedirect(self.submit_url()) 

110 if self.add_confirm_message: 

111 messages.info(self.request, _("Please confirm the submission of the form.")) 

112 

113 resp = self.form_post_save(form, resp) 

114 

115 return resp 

116 

117 def get_context_data(self, *args, **kwargs): 

118 context = super().get_context_data(*args, **kwargs) 

119 context["submit_confirm"] = self._submit_confirm 

120 return context 

121 

122 def get(self, request, *args, **kwargs): 

123 """ 

124 Overloads the default get to catch whether it's a submit confirmation 

125 request. 

126 """ 

127 self._submit_confirm = self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true" 

128 return super().get(request, *args, **kwargs) 

129 

130 def get_form_kwargs(self) -> dict[str, Any]: 

131 kwargs = super().get_form_kwargs() 

132 if self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true": 

133 kwargs[SUBMIT_QUERY_PARAMETER] = True 

134 return kwargs 

135 

136 

137class MeshObjectMixin: 

138 _submission = None 

139 _review = None 

140 _version = None 

141 _decision = None 

142 

143 def get_queryset(self): 

144 return self.request.current_role.get_submissions() 

145 

146 # submission__pk 

147 def get_submission(self, queryset=None) -> "Submission": 

148 if self._submission is not None: 

149 return self._submission 

150 if queryset is None: 

151 queryset = self.get_queryset() 

152 submission_pk = self.kwargs.get("submission_pk") 

153 

154 try: 

155 self._submission = queryset.get(pk=submission_pk) 

156 return self._submission 

157 except queryset.model.DoesNotExist: 

158 raise Http404( 

159 _("No %(verbose_name)s found matching the query") 

160 % {"verbose_name": queryset.model._meta.verbose_name} 

161 ) 

162 

163 # version_pk 

164 def get_version(self, queryset=None): 

165 if self._version is not None: 

166 return self._version 

167 version_pk = self.kwargs.get("version_pk") 

168 

169 submission = self.get_submission(queryset) 

170 

171 # Using python iterator for now 

172 # If this causes a performance hit, using a filter directly on the prefetched queryset should be a good idea 

173 version = next((v for v in submission.versions_censored if v.pk == version_pk), None) 

174 if not version: 

175 raise Http404( 

176 _("No %(verbose_name)s found matching the query") 

177 % {"verbose_name": SubmissionVersion._meta.verbose_name} 

178 ) 

179 return version 

180 

181 # review_pk 

182 def get_review(self, queryset=None): 

183 if self._review is not None: 

184 return self._review 

185 review_pk = self.kwargs.get("review_pk") 

186 version = self.get_version(queryset) 

187 review = next((r for r in version.reviews_censored if r.pk == review_pk), None) 

188 if not review: 

189 raise Http404( 

190 _("No %(verbose_name)s found matching the query") 

191 % {"verbose_name": Review._meta.verbose_name} 

192 ) 

193 return review 

194 

195 def get_decision(self, queryset=None) -> EditorialDecision: 

196 if self._decision is not None: 

197 return self._decision 

198 version = self.get_version(queryset) 

199 decision = getattr(version, "editorial_decision", None) 

200 if not decision: 

201 raise Http404( 

202 _("No %(verbose_name)s found matching the query") 

203 % {"verbose_name": EditorialDecision._meta.verbose_name} 

204 ) 

205 return decision