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
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-09 13:14 +0000
1from typing import Any
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
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)
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
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.
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`
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 """
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
59 def submit_url(self) -> str:
60 """
61 URL to redirect to to when the user submits the form.
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 )
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
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.
84 This must return an HTTP response.
85 """
86 return original_response
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()
104 self.form_pre_save(form)
106 resp = super().form_valid(form)
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."))
113 resp = self.form_post_save(form, resp)
115 return resp
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
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)
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
137class MeshObjectMixin:
138 _submission = None
139 _review = None
140 _version = None
141 _decision = None
143 def get_queryset(self):
144 return self.request.current_role.get_submissions()
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")
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 )
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")
169 submission = self.get_submission(queryset)
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
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
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