Coverage for src/mesh/views/views_submission.py: 49%
166 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
1from collections.abc import Iterable
2from typing import TYPE_CHECKING, Any
4from django.http import HttpRequest, JsonResponse
5from django.shortcuts import get_object_or_404, render
6from django.urls import reverse, reverse_lazy
7from django.utils.decorators import method_decorator
8from django.utils.translation import gettext_lazy as _
9from django.views.decorators.csrf import csrf_exempt
10from django.views.generic import TemplateView, View
11from ptf.url_utils import format_url_with_params
13from ..app_settings import app_settings
14from ..model.filters import FieldGetter, Filter, FilterSet
15from ..model.roles.editor import Editor
16from ..model.roles.journal_manager import JournalManager
17from ..model.roles.role_handler import RoleHandler
18from ..models.journal_models import JournalSection
19from ..models.submission_models import Submission, SubmissionLog, SubmissionState
20from ..views.mixins import RoleMixin
21from .components.breadcrumb import get_base_breadcrumb, get_submission_breadcrumb
22from .components.ckeditor_config import sanitize_html_input
23from .components.review_summary import CountWithTotal
24from .components.submission_list import (
25 SubmissionListEnum,
26 get_all_submission_list_config,
27 get_done_submission_list_config,
28 get_submission_by_state_config,
29 get_submission_list_config,
30)
31from .model_proxy import BuildSubmissionProxyVisitor
32from .utils import group_by
34if TYPE_CHECKING: 34 ↛ 35line 34 didn't jump to line 35 because the condition on line 34 was never true
35 from mesh.views.components.submission_list import SubmissionListConfig
38def get_submission_message_if_no_actions(submission, role_handler):
39 """
40 An author can not do anything if the submission is under review.
41 Instead, we display a "thank you" message
42 """
44 html_message = ""
45 if not role_handler.check_rights("can_edit_submission", submission):
46 contact_email = app_settings.JOURNAL_EMAIL_CONTACT
47 html_message = f"""
48<p>Thank you for submitting your article.</p>
49We will review your submission and get back to you. You can contact us at {contact_email}"""
51 return html_message
54class SubmissionDetailsView(RoleMixin, TemplateView):
55 """
56 View for a single submission.
58 The context data is populated with all the available actions to the user
59 regarding the submission.
60 """
62 template_name = "mesh/submission/submission_details.html"
63 submission: Submission
65 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
66 # TODO: Should we prefetch all data for an unique instance ?
67 # It would result in the same amount of queries but, with prefetch, they would
68 # all be made at the same time.
69 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
70 return not self.role_handler.check_rights("can_access_submission", self.submission)
72 def get_context_data(self, **kwargs) -> dict[str, Any]:
73 context = super().get_context_data(**kwargs)
75 builder = BuildSubmissionProxyVisitor(self.role_handler)
77 context["submission_proxy"] = self.role_handler.current_role.accept(
78 builder,
79 self.submission,
80 display_as_btn=False,
81 reload_page=True,
82 shortlist=False,
83 )
85 # editorial_decisions = (
86 # self.submission.editorial_decisions.all().order_by( # type:ignore
87 # "-date_created"
88 # )
89 # )
90 #
91 # context["editorial_decisions"] = editorial_decisions
93 # review_form = StartReviewProcessForm(initial={"process": True})
94 # context["action_lists"] = build_submission_actions(
95 # self.submission, self.role_handler, review_form
96 # )
97 context["message_if_no_actions"] = get_submission_message_if_no_actions(
98 self.submission, self.role_handler
99 )
101 # # Generate breadcrumb data
102 # context["breadcrumb"] = get_submission_breadcrumb(self.submission)
103 return context
106def submission_list_filters() -> FilterSet:
107 """
108 Instantiate a fresh FilterSet for the submission list view.
109 """
110 return FilterSet(
111 id="submission-filters",
112 name="Filters",
113 filters=[
114 Filter(id="created_by", name=_("Author"), type="model"),
115 Filter(
116 id="state",
117 name=_("State"),
118 type="string",
119 name_getter=FieldGetter(attr="get_state_display", callable=True),
120 ),
121 Filter(id="journal_section", name=_("Section"), type="model"),
122 Filter(id="all_assigned_editors", name=_("Assigned editors"), type="model"),
123 ],
124 )
127def group_submissions_per_status(
128 submissions: Iterable[Submission], role_handler: RoleHandler
129) -> dict[Any, list[Submission]]:
130 return group_by(
131 submissions,
132 lambda s: role_handler.get_from_rights("get_submission_status", s).status,
133 )
136def group_submissions_per_state(
137 submissions: Iterable[Submission], role_handler: RoleHandler
138) -> dict[Any, list[Submission]]:
139 return group_by(
140 submissions,
141 lambda s: SubmissionState(s.state),
142 )
145def one_group_submissions(submissions, role_handler):
146 return {SubmissionListEnum.ALL: submissions}
149def prepare_submissions_lists(
150 role_handler: RoleHandler,
151 request: HttpRequest,
152 list_config_ftor=get_submission_list_config,
153 group_submissions_ftor=group_submissions_per_status,
154) -> dict[str, Any]:
155 """
156 Retrieve, group and filter the user's submissions to show according to:
157 - the user's role submission list config
158 - the applied filters (contained in the HTTP request)
160 Params:
161 - `role_handler` The role handler
162 - `request` The underlying HTTP request
163 - `config_key` The right function to be called to get the SubmissionListConfig(s)
164 to be used. This config defines which submissions to display along
165 with filtering behavior, etc.
166 """
167 context: dict[str, Any] = {}
169 # The obtained submissions should have prefetched their related data.
170 # It might be used extensively in the filtering and rendering processes.
171 all_submissions = role_handler.get_attribute("submissions")
172 # The submissions are displayed in different tables grouped by status. Precise
173 # behavior is configured per role with SubmissionListConfig objects.
174 grouped_submissions = group_submissions_ftor(all_submissions, role_handler)
176 submission_list_configs: list[SubmissionListConfig] = list_config_ftor()
177 for config in submission_list_configs:
178 config.all_submissions = grouped_submissions.get(config.key, [])
180 builder = BuildSubmissionProxyVisitor(role_handler)
182 # Handle the filters per submission list
183 if role_handler.check_rights("can_filter_submissions"): 183 ↛ 209line 183 didn't jump to line 209 because the condition on line 183 was always true
184 # Init filters
185 filters = submission_list_filters()
186 filters.init_filters(
187 request.GET,
188 *(config.all_submissions for config in submission_list_configs if config.in_filters),
189 )
190 context["filters"] = filters
191 # Filter all submission lists
192 for config in submission_list_configs:
193 if not config.display:
194 continue
195 if not config.in_filters:
196 config.submissions = [
197 role_handler.current_role.accept(
198 builder, s, row_id=f"{config.id}-row-{counter}"
199 )
200 for counter, s in enumerate(config.all_submissions)
201 ]
202 continue
203 config.submissions = [
204 role_handler.current_role.accept(builder, s, row_id=f"{config.id}-row-{counter}")
205 for counter, s in enumerate(filters.filter(config.all_submissions))
206 ]
208 else:
209 for config in submission_list_configs:
210 if not config.display:
211 continue
212 config.submissions = [
213 role_handler.current_role.accept(builder, s, row_id=f"{config.id}-row-{counter}")
214 for counter, s in enumerate(config.all_submissions)
215 ]
217 context["submissions_config"] = submission_list_configs
218 context["all_submissions_count"] = sum(
219 len(config.all_submissions) for config in submission_list_configs
220 )
221 context["submission_count"] = CountWithTotal(
222 value=sum(
223 len(config.submissions)
224 for config in submission_list_configs
225 if config.display and config.in_filters
226 ),
227 total=sum(
228 len(config.all_submissions)
229 for config in submission_list_configs
230 if config.display and config.in_filters
231 ),
232 )
234 return context
237def all_role_submissions_count(role_handler: RoleHandler) -> list[dict]:
238 """
239 Returns the total number of submissions per active role.
240 """
241 all_role_submissions = []
243 for role in role_handler.role_data.get_active_roles():
244 role_submissions = {
245 "role": role.summary().serialize(),
246 "submissions": len(role.rights.submissions),
247 }
248 if role == role_handler.current_role:
249 role_submissions["active"] = True
250 all_role_submissions.append(role_submissions)
252 return all_role_submissions
255class SubmissionListView(RoleMixin, TemplateView):
256 """
257 View for the list of all submissions currently accessible to the user role, grouped by Status
258 """
260 template_name = "mesh/submission/submission_list_page.html"
262 def get_context_data(self, **kwargs) -> dict[str, Any]:
263 context = super().get_context_data(**kwargs)
265 context.update(prepare_submissions_lists(self.role_handler, self.request))
266 all_role_submissions = all_role_submissions_count(self.role_handler)
267 if len(all_role_submissions) > 1:
268 context["all_role_submissions"] = all_role_submissions
270 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
272 context["active_tab"] = "status"
274 # # Generate breadcrumb data
275 # breadcrumb = get_base_breadcrumb()
276 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list"))
277 # context["breadcrumb"] = breadcrumb
278 return context
281class AllSubmissionsListView(RoleMixin, TemplateView):
282 """
283 View for the list of all submissions currently accessible to the user role, in 1 flat list
284 """
286 template_name = "mesh/submission/submission_list_page.html"
288 def get_context_data(self, **kwargs) -> dict[str, Any]:
289 context = super().get_context_data(**kwargs)
291 context.update(
292 prepare_submissions_lists(
293 self.role_handler,
294 self.request,
295 list_config_ftor=get_all_submission_list_config,
296 group_submissions_ftor=one_group_submissions,
297 )
298 )
300 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
302 context["active_tab"] = "all"
304 # # Generate breadcrumb data
305 # breadcrumb = get_base_breadcrumb()
306 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list"))
307 # context["breadcrumb"] = breadcrumb
308 return context
311class SubmissionsByStateListView(RoleMixin, TemplateView):
312 """
313 View for the list of all submissions currently accessible to the user role, in 1 flat list
314 """
316 template_name = "mesh/submission/submission_list_page.html"
318 def get_context_data(self, **kwargs) -> dict[str, Any]:
319 context = super().get_context_data(**kwargs)
321 context.update(
322 prepare_submissions_lists(
323 self.role_handler,
324 self.request,
325 list_config_ftor=get_submission_by_state_config,
326 group_submissions_ftor=group_submissions_per_state,
327 )
328 )
330 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
332 context["active_tab"] = "state"
334 # # Generate breadcrumb data
335 # breadcrumb = get_base_breadcrumb()
336 # breadcrumb.add_item(title=_("My submissions"), url=reverse_lazy("mesh:submission_list"))
337 # context["breadcrumb"] = breadcrumb
338 return context
341class DoneSubmissionListView(RoleMixin, TemplateView):
342 restricted_roles = [JournalManager, Editor]
343 template_name = "mesh/submission/submission_list_page.html"
345 def get_context_data(self, *args, **kwargs) -> dict[str, Any]:
346 context = super().get_context_data(*args, **kwargs)
347 context.update(
348 prepare_submissions_lists(
349 self.role_handler,
350 self.request,
351 list_config_ftor=get_done_submission_list_config,
352 )
353 )
354 context["done_page"] = True
356 # Generate breadcrumb data
357 breadcrumb = get_base_breadcrumb()
358 breadcrumb.add_item(
359 title=_("Done submissions"),
360 url=reverse_lazy("mesh:submission_list_done"),
361 )
362 context["breadcrumb"] = breadcrumb
363 return context
366class SubmissionLogView(RoleMixin, TemplateView):
367 """
368 View to display the `SubmissionLog` of a submission.
369 """
371 template_name = "mesh/log.html"
372 submission: Submission
374 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
375 pk = kwargs["pk"]
376 self.submission = get_object_or_404(Submission, pk=pk)
378 return not self.role_handler.check_rights("can_access_submission_log", self.submission)
380 def get_context_data(self, *args, **kwargs):
381 context = super().get_context_data(*args, **kwargs)
383 logs = SubmissionLog.objects.filter(attached_to=self.submission)
384 order = self.request.GET.get("order")
385 context["order"] = order
386 if order == "asc":
387 logs = logs.order_by("date_created")
388 else:
389 logs = logs.order_by("-date_created")
390 context["logs"] = logs
392 query_params = {"order": "desc" if order == "asc" else "asc"}
393 sort_url = reverse("mesh:submission_log", kwargs={"pk": self.submission.pk})
394 context["sort_url"] = format_url_with_params(sort_url, query_params)
396 context["page_title"] = _("Submission history")
398 # Generate breadcrumb data
399 breadcrumb = get_submission_breadcrumb(self.submission)
400 breadcrumb.add_item(
401 title=_("Submission history"),
402 url=reverse_lazy("mesh:submission_log", kwargs={"pk": self.submission.pk}),
403 )
404 context["breadcrumb"] = breadcrumb
405 return context
408class SubmissionInListAPIView(RoleMixin, View):
409 def get(self, request, *args, **kwargs):
410 submission = get_object_or_404(Submission, pk=kwargs["pk"])
411 row_id = kwargs["row_id"]
413 builder = BuildSubmissionProxyVisitor(self.role_handler)
414 submission_proxy = self.role_handler.current_role.accept(
415 builder, submission, row_id=row_id
416 )
418 context = {
419 "submission_proxy": submission_proxy,
420 "role_handler": self.role_handler,
421 "with_journal_sections": JournalSection.objects.all_journal_sections().exists(),
422 }
424 return render(
425 request,
426 "mesh/submission/submission_table_row.html",
427 context,
428 )
431@method_decorator([csrf_exempt], name="dispatch")
432class SubmissionNotesAPIView(RoleMixin, View):
433 def post(self, request, *args, **kwargs):
434 submission = get_object_or_404(Submission, pk=kwargs["pk"])
436 notes = request.POST.get("editabledata", "")
437 sanitized_notes = sanitize_html_input(notes)
439 submission.notes = sanitized_notes
440 submission.save()
442 return JsonResponse({"message": "OK"})