Coverage for src / mesh / views / views_submission.py: 61%
171 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-05-04 12:41 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-05-04 12:41 +0000
1from collections.abc import Iterable
2from typing import TYPE_CHECKING, Any
4from django.contrib.auth.mixins import LoginRequiredMixin
5from django.core.exceptions import PermissionDenied
6from django.http import Http404, JsonResponse
7from django.shortcuts import render
8from django.urls import reverse
9from django.utils.decorators import method_decorator
10from django.utils.translation import gettext_lazy as _
11from django.views.decorators.csrf import csrf_exempt
12from django.views.generic import DetailView, TemplateView
13from opentelemetry import trace
14from ptf.url_utils import format_url_with_params
16from mesh.app_settings import app_settings
17from mesh.models.filters import FieldGetter, Filter, FilterSet
18from mesh.models.orm.journal_models import JournalSection
19from mesh.models.orm.submission_models import Submission, SubmissionState
20from mesh.models.roles.editor import Editor
21from mesh.models.roles.journal_manager import JournalManager
22from mesh.views.components.breadcrumb import get_base_breadcrumb, get_submission_breadcrumb
23from mesh.views.components.ckeditor_config import sanitize_html_input
24from mesh.views.components.review_summary import CountWithTotal
25from mesh.views.components.submission_list import (
26 SubmissionListEnum,
27 get_all_submission_list_config,
28 get_done_submission_list_config,
29 get_submission_by_state_config,
30 get_submission_list_config,
31)
32from mesh.views.utils import group_by
33from mesh.views.viewmodel.submission_proxy import BuildSubmissionProxyVisitor
34from mesh.views.views_base import MeshObjectMixin
36if TYPE_CHECKING:
37 from django.http import HttpRequest
39 from mesh.models.roles.base_role import Role
40 from mesh.views.components.submission_list import SubmissionListConfig
42tracer = trace.get_tracer(__name__)
45def get_submission_message_if_no_actions(submission, role: "Role"):
46 """
47 An author can not do anything if the submission is under review.
48 Instead, we display a "thank you" message
49 """
51 html_message = ""
52 if not role.can_edit_submission(submission):
53 contact_email = app_settings.JOURNAL_EMAIL_CONTACT
54 html_message = f"""
55<p>Thank you for submitting your article.</p>
56We will review your submission and get back to you. You can contact us at {contact_email}"""
58 return html_message
61class SubmissionDetailsView(LoginRequiredMixin, MeshObjectMixin, DetailView):
62 """
63 View for a single submission.
65 The context data is populated with all the available actions to the user
66 regarding the submission.
67 """
69 template_name = "mesh/submission/submission_details.html"
71 def setup(self, request, *args, **kwargs):
72 super().setup(request, *args, **kwargs)
74 @tracer.start_as_current_span("SubmissionDetailsView.get_object")
75 def get_object(self, queryset=None):
76 ojsid = self.kwargs.get("ojsid", None)
77 if ojsid:
78 if queryset is None:
79 queryset = self.get_queryset()
80 try:
81 return queryset.get(ojs_id=ojsid)
82 except queryset.model.DoesNotExist:
83 raise Http404(
84 _("No %(verbose_name)s found matching the query")
85 % {"verbose_name": queryset.model._meta.verbose_name}
86 )
87 return self.get_submission()
89 # def restrict_dispatch(self, request: "HttpRequest", *args, **kwargs):
90 # # TODO: Should we prefetch all data for an unique instance ?
91 # # It would result in the same amount of queries but, with prefetch, they would
92 # # all be made at the same time.
93 # self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
94 # return not request.current_role.can_access_submission(self.submission)
96 def get_context_data(self, object: Submission, **kwargs) -> dict[str, Any]:
97 context = super().get_context_data(**kwargs)
98 # BaseDetailView already calls get_object in the get method
99 submission = object
100 builder = BuildSubmissionProxyVisitor(self.request.current_role)
102 context["submission_proxy"] = builder.visit(
103 submission,
104 display_as_btn=False,
105 reload_page=True,
106 shortlist=False,
107 )
109 # editorial_decisions = (
110 # self.submission.editorial_decisions.all().order_by(
111 # "-date_created"
112 # )
113 # )
114 #
115 # context["editorial_decisions"] = editorial_decisions
117 # review_form = StartReviewProcessForm(initial={"process": True})
118 # context["action_lists"] = build_submission_actions(
119 # self.submission, self.role_handler, review_form
120 # )
121 context["message_if_no_actions"] = get_submission_message_if_no_actions(
122 submission, self.request.current_role
123 )
125 # # Generate breadcrumb data
126 # context["breadcrumb"] = get_submission_breadcrumb(self.submission)
127 return context
130def submission_list_filters() -> FilterSet:
131 """
132 Instantiate a fresh FilterSet for the submission list view.
133 """
134 return FilterSet(
135 id="submission-filters",
136 name="Filters",
137 filters=[
138 Filter(id="created_by", name=_("Author"), type="model"),
139 Filter(
140 id="state",
141 name=_("State"),
142 type="string",
143 name_getter=FieldGetter(attr="get_state_display", callable=True),
144 ),
145 Filter(id="journal_section", name=_("Section"), type="model"),
146 Filter(id="all_assigned_editors", name=_("Assigned editors"), type="model"),
147 ],
148 )
151def group_submissions_per_status(submissions: Iterable[Submission], role: "Role"):
152 return group_by(
153 submissions,
154 lambda s: role.get_submission_status(s).status,
155 )
158def group_submissions_per_state(submissions: Iterable[Submission], role: "Role"):
159 return group_by(
160 submissions,
161 lambda s: SubmissionState(s.state),
162 )
165def one_group_submissions(submissions: Iterable[Submission], role: "Role"):
166 return {SubmissionListEnum.ALL: submissions}
169@tracer.start_as_current_span("prepare_submissions_lists")
170def prepare_submissions_lists(
171 role: "Role",
172 request: "HttpRequest",
173 list_config_ftor=get_submission_list_config,
174 group_submissions_ftor=group_submissions_per_status,
175) -> dict[str, Any]:
176 """
177 Retrieve, group and filter the user's submissions to show according to:
178 - the user's role submission list config
179 - the applied filters (contained in the HTTP request)
181 Params:
182 - `role_handler` The role handler
183 - `request` The underlying HTTP request
184 - `config_key` The right function to be called to get the SubmissionListConfig(s)
185 to be used. This config defines which submissions to display along
186 with filtering behavior, etc.
187 """
188 context: dict[str, Any] = {}
189 with tracer.start_as_current_span("prepare_submissions_lists.evaluate_queryset"):
190 submissions_qs = list(role.get_submissions())
192 all_submissions = []
193 for s in submissions_qs:
194 s.status = role.get_submission_status(s)
195 all_submissions.append(s)
197 # The submissions are displayed in different tables grouped by status. Precise
198 # behavior is configured per role with SubmissionListConfig objects.
199 grouped_submissions = group_submissions_ftor(all_submissions, role)
201 submission_list_configs: list[SubmissionListConfig] = list_config_ftor()
202 for config in submission_list_configs:
203 config.all_submissions = grouped_submissions.get(config.key, [])
205 builder = BuildSubmissionProxyVisitor(role)
207 # Handle the filters per submission list
208 if role.can_filter_submissions():
209 # Init filters
210 filters = submission_list_filters()
211 filters.init_filters(
212 request.GET,
213 *(config.all_submissions for config in submission_list_configs if config.in_filters),
214 )
215 context["filters"] = filters
216 # Filter all submission lists
217 for config in submission_list_configs:
218 if not config.display:
219 continue
220 if not config.in_filters:
221 config.submissions = [
222 builder.visit(s, row_id=f"{config.id}-row-{counter}")
223 for counter, s in enumerate(config.all_submissions)
224 ]
225 continue
226 config.submissions = [
227 builder.visit(s, row_id=f"{config.id}-row-{counter}")
228 for counter, s in enumerate(filters.filter(config.all_submissions))
229 ]
231 else:
232 for config in submission_list_configs:
233 if not config.display:
234 continue
235 config.submissions = [
236 role.accept(builder, s, row_id=f"{config.id}-row-{counter}")
237 for counter, s in enumerate(config.all_submissions)
238 ]
240 context["submissions_config"] = submission_list_configs
241 context["all_submissions_count"] = sum(
242 len(config.all_submissions) for config in submission_list_configs
243 )
244 context["submission_count"] = CountWithTotal(
245 value=sum(
246 len(config.submissions)
247 for config in submission_list_configs
248 if config.display and config.in_filters
249 ),
250 total=sum(
251 len(config.all_submissions)
252 for config in submission_list_configs
253 if config.display and config.in_filters
254 ),
255 )
257 return context
260# def all_role_submissions_count(role: Role) -> list[dict]:
261# """
262# Returns the total number of submissions per active role.
263# """
264# all_role_submissions = []
266# for role in role.role_data.get_active_roles():
267# role_submissions = {
268# "role": role.summary().serialize(),
269# "submissions": len(role.get_submissions()),
270# }
271# if role == role_handler.current_role:
272# role_submissions["active"] = True
273# all_role_submissions.append(role_submissions)
275# return all_role_submissions
278class SubmissionListView(LoginRequiredMixin, TemplateView):
279 """
280 View for the list of all submissions currently accessible to the user role, grouped by Status
281 """
283 template_name = "mesh/submission/submission_list_page.html"
285 def get_context_data(self, **kwargs) -> dict[str, Any]:
286 context = super().get_context_data(**kwargs)
288 context.update(prepare_submissions_lists(self.request.current_role, self.request))
289 # all_role_submissions = all_role_submissions_count(self.request.current_role)
290 # if len(all_role_submissions) > 1:
291 # context["all_role_submissions"] = all_role_submissions
293 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
295 context["active_tab"] = "status"
296 context["role"] = self.request.current_role
297 # # Generate breadcrumb data
298 # breadcrumb = get_base_breadcrumb()
299 # breadcrumb.add_item(title=_("My submissions"), url=reverse("mesh:submission_list"))
300 # context["breadcrumb"] = breadcrumb
301 return context
304class AllSubmissionsListView(LoginRequiredMixin, TemplateView):
305 """
306 View for the list of all submissions currently accessible to the user role, in 1 flat list
307 """
309 template_name = "mesh/submission/submission_list_page.html"
311 def get_context_data(self, **kwargs) -> dict[str, Any]:
312 context = super().get_context_data(**kwargs)
314 context.update(
315 prepare_submissions_lists(
316 self.request.current_role,
317 self.request,
318 list_config_ftor=get_all_submission_list_config,
319 group_submissions_ftor=one_group_submissions,
320 )
321 )
323 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
325 context["active_tab"] = "all"
327 # # Generate breadcrumb data
328 # breadcrumb = get_base_breadcrumb()
329 # breadcrumb.add_item(title=_("My submissions"), url=reverse("mesh:submission_list"))
330 # context["breadcrumb"] = breadcrumb
331 return context
334class SubmissionsByStateListView(LoginRequiredMixin, TemplateView):
335 """
336 View for the list of all submissions currently accessible to the user role, in 1 flat list
337 """
339 template_name = "mesh/submission/submission_list_page.html"
341 def get_context_data(self, **kwargs) -> dict[str, Any]:
342 context = super().get_context_data(**kwargs)
344 context.update(
345 prepare_submissions_lists(
346 self.request.current_role,
347 self.request,
348 list_config_ftor=get_submission_by_state_config,
349 group_submissions_ftor=group_submissions_per_state,
350 )
351 )
353 context["with_journal_sections"] = JournalSection.objects.all_journal_sections().exists()
355 context["active_tab"] = "state"
357 # # Generate breadcrumb data
358 # breadcrumb = get_base_breadcrumb()
359 # breadcrumb.add_item(title=_("My submissions"), url=reverse("mesh:submission_list"))
360 # context["breadcrumb"] = breadcrumb
361 return context
364class DoneSubmissionListView(LoginRequiredMixin, TemplateView):
365 restricted_roles = [JournalManager, Editor]
366 template_name = "mesh/submission/submission_list_page.html"
368 def get_context_data(self, *args, **kwargs) -> dict[str, Any]:
369 context = super().get_context_data(*args, **kwargs)
370 context.update(
371 prepare_submissions_lists(
372 self.request.current_role,
373 self.request,
374 list_config_ftor=get_done_submission_list_config,
375 )
376 )
377 context["done_page"] = True
379 # Generate breadcrumb data
380 breadcrumb = get_base_breadcrumb()
381 breadcrumb.add_item(
382 title=_("Done submissions"),
383 url=reverse("mesh:submission_list_done"),
384 )
385 context["breadcrumb"] = breadcrumb
386 return context
389class SubmissionLogView(LoginRequiredMixin, MeshObjectMixin, DetailView):
390 """
391 View to display the `SubmissionLog` of a submission.
392 """
394 template_name = "mesh/log.html"
396 def get_object(self, queryset=None):
397 return self.get_submission()
399 def get_context_data(self, *args, **kwargs):
400 context = super().get_context_data(*args, **kwargs)
401 submission: Submission = self.get_object()
402 logs = submission.log_messages_censored
404 order = self.request.GET.get("order")
405 context["order"] = order
406 # TODO : Investigate this : This may be inverted
407 if order == "asc":
408 logs.reverse()
409 context["logs"] = logs
411 query_params = {"order": "desc" if order == "asc" else "asc"}
412 sort_url = reverse("mesh:submission_log", kwargs={"pk": submission.pk})
413 context["sort_url"] = format_url_with_params(sort_url, query_params)
415 context["page_title"] = _("Submission history")
417 # Generate breadcrumb data
418 breadcrumb = get_submission_breadcrumb(submission)
419 breadcrumb.add_item(
420 title=_("Submission history"),
421 url=reverse("mesh:submission_log", kwargs={"pk": submission.pk}),
422 )
423 context["breadcrumb"] = breadcrumb
424 return context
427class SubmissionInListAPIView(LoginRequiredMixin, MeshObjectMixin, DetailView):
428 def get(self, request, *args, **kwargs):
429 submission = self.get_submission()
431 builder = BuildSubmissionProxyVisitor(self.request.current_role)
432 submission_proxy = builder.visit(submission, row_id=kwargs["row_id"])
434 context = {
435 "submission_proxy": submission_proxy,
436 "with_journal_sections": JournalSection.objects.all_journal_sections().exists(),
437 }
439 return render(
440 request,
441 "mesh/submission/submission_table_row.html",
442 context,
443 )
446@method_decorator([csrf_exempt], name="dispatch")
447class SubmissionNotesAPIView(LoginRequiredMixin, MeshObjectMixin, DetailView):
448 def post(self, request, *args, **kwargs):
449 submission = self.get_submission()
451 if not self.request.current_role.can_edit_submission(submission):
452 raise PermissionDenied
454 notes = request.POST.get("editabledata", "")
455 sanitized_notes = sanitize_html_input(notes)
457 submission.notes = sanitized_notes
458 submission.save()
460 return JsonResponse({"message": "OK"})