Coverage for src/mesh/views/views_submission_edit.py: 29%
306 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 functools import cached_property
2from typing import Any
4from django.contrib import messages
5from django.db.models import QuerySet
6from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
7from django.shortcuts import get_object_or_404
8from django.urls import reverse_lazy
9from django.utils.translation import gettext_lazy as _
10from django.views.generic import FormView, TemplateView, View
11from django.views.generic.edit import CreateView, UpdateView
13from mesh.model.file_helpers import post_delete_model_file
14from mesh.model.roles.author import Author
15from mesh.views.forms.base_forms import FormAction, HiddenModelChoiceForm
16from mesh.views.forms.submission_forms import (
17 SubmissionAuthorForm,
18 SubmissionConfirmForm,
19 SubmissionCreateForm,
20 SubmissionUpdateForm,
21 SubmissionVersionForm,
22)
23from mesh.views.mixins import RoleMixin
25from ..models.review_models import ReviewState
26from ..models.submission_models import (
27 Submission,
28 SubmissionAuthor,
29 SubmissionLog,
30 SubmissionVersion,
31)
32from ..models.user_models import Suggestion
33from ..views.views_base import SUBMIT_QUERY_PARAMETER, SubmittableModelFormMixin
34from ..views.views_reviewer import add_suggestion
36# from .components.breadcrumb import get_base_breadcrumb
37# from .components.breadcrumb import get_submission_breadcrumb
38from .components.button import Button
39from .components.stepper import get_submission_stepper
40from .model_proxy import SubmissionProxy
42# def submission_stepper_form_buttons() -> list[Button]:
43# return [
44# Button(
45# id="form_save",
46# title=_("Save as draft"),
47# icon_class="fa-floppy-disk",
48# attrs={"type": ["submit"], "class": ["save-button", "light"]},
49# ),
50# Button(
51# id="form_next",
52# title=_("Next"),
53# icon_class="fa-right-long",
54# attrs={"type": ["submit"], "class": ["save-button"], "name": [FormAction.NEXT.value]},
55# ),
56# ]
59class SubmissionCreateView(RoleMixin, CreateView):
60 """
61 View for submitting a new submission.
62 """
64 model = Submission
65 form_class = SubmissionCreateForm
66 template_name = "mesh/forms/form_full_page.html"
67 restricted_roles = [Author]
69 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
70 return not self.role_handler.check_rights("can_create_submission")
72 def get_success_url(self) -> str:
73 """
74 Redirects to the SubmissionAuthor view to add authors to the
75 newly created submission.
76 """
77 if FormAction.NEXT.value in self.request.POST:
78 return reverse_lazy(
79 "mesh:submission_authors",
80 kwargs={"pk": self.object.pk}, # type:ignore
81 )
82 return reverse_lazy(
83 "mesh:submission_details",
84 kwargs={"pk": self.object.pk}, # type:ignore
85 )
87 def form_valid(self, form: SubmissionCreateForm) -> HttpResponse:
88 """
89 Injects the current user in the form instance.
90 Adds a message to the submission log.
91 """
92 form.instance._user = self.request.user
94 response = super().form_valid(form)
96 SubmissionLog.add_message(
97 self.object, # type:ignore
98 content=_("Creation of the submission"),
99 content_en="Creation of the submission",
100 user=self.request.user,
101 significant=True,
102 )
104 messages.success(self.request, _("New submission successfully created."))
105 return response
107 def get_context_data(self, *args, **kwargs):
108 context = super().get_context_data(*args, **kwargs)
109 context["page_title"] = _("New submission")
111 # Stepper
112 step_id = "metadata"
113 stepper = get_submission_stepper(None)
114 stepper.set_active_step(step_id)
115 context["stepper"] = stepper
117 # Form buttons
118 # No Save/Next button in the form, they are put in the stepper
119 context["form"].buttons = [] # = submission_stepper_form_buttons()
121 descriptions = [
122 "You are about to start the submit process.",
123 'Please read our guidelines <a href="">here</a>',
124 ]
125 context["form_description"] = descriptions
127 # # Generate breadcrumb data
128 # breadcrumb = get_base_breadcrumb()
129 # breadcrumb.add_item(title=_("New submission"), url=reverse_lazy("mesh:submission_create"))
130 # context["breadcrumb"] = breadcrumb
131 return context
134class SubmissionUpdateView(RoleMixin, UpdateView):
135 """
136 View for updating a submission (only the metadata).
137 """
139 model: type[Submission] = Submission
140 form_class = SubmissionUpdateForm
141 template_name = "mesh/forms/form_full_page.html"
142 submission: Submission
143 restricted_roles = [Author]
144 message_on_restrict = False
146 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
147 pk = kwargs["pk"]
148 self.submission = get_object_or_404(self.model, pk=pk)
150 return not self.role_handler.check_rights("can_edit_submission", self.submission)
152 def get_fail_redirect_uri(self) -> str:
153 """
154 Return to the submission details page if the user cannot update the submission.
155 """
156 return self.get_success_url()
158 def get_object(self, *args, **kwargs) -> Submission:
159 """
160 Override `get_object` built-in method to avoid an additional look-up when
161 we already have the object loaded.
162 """
163 return self.submission
165 def get_context_data(self, *args, **kwargs):
166 context = super().get_context_data(*args, **kwargs)
167 context["page_title"] = _("Edit article metadata")
169 if self.submission.is_draft:
170 step_id = "metadata"
171 stepper = get_submission_stepper(self.submission)
172 stepper.set_active_step(step_id)
173 context["stepper"] = stepper
175 # No Save/Next button in the form, they are put in the stepper
176 context["form"].buttons = [] # = submission_stepper_form_buttons()
178 # Generate breadcrumb data
179 # breadcrumb = get_submission_breadcrumb(self.submission)
180 # breadcrumb.add_item(
181 # title=_("Edit metadata"),
182 # url=reverse_lazy("mesh:submission_update", kwargs={"pk": self.submission.pk}),
183 # )
184 # context["breadcrumb"] = breadcrumb
186 return context
188 def get_success_url(self) -> str:
189 if FormAction.NEXT.value in self.request.POST:
190 return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
191 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
193 def form_valid(self, form: SubmissionUpdateForm) -> HttpResponse:
194 form.instance._user = self.request.user
195 if not form.changed_data:
196 form.instance.do_not_update = True
198 response = super().form_valid(form)
200 if form.has_changed():
201 SubmissionLog.add_message(
202 self.submission,
203 content=_("Modification of the submission's metadata"),
204 content_en="Modification of the submission's metadata",
205 user=self.request.user,
206 )
207 return response
210class SubmissionVersionCreateView(RoleMixin, SubmittableModelFormMixin, CreateView):
211 """
212 View for creating a new submission version.
213 """
215 model = SubmissionVersion
216 form_class = SubmissionVersionForm
217 template_name = "mesh/forms/form_full_page.html"
218 submission: Submission
219 restricted_roles = [Author]
220 save_submission = False
221 add_confirm_message = False
223 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
224 submission_pk = kwargs["submission_pk"]
225 self.submission = get_object_or_404(Submission, pk=submission_pk)
227 return not self.role_handler.check_rights("can_create_version", self.submission)
229 def get_success_url(self) -> str:
230 if FormAction.NEXT.value in self.request.POST:
231 return self.submit_url()
232 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
234 def submit_url(self) -> str:
235 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk})
237 def form_pre_save(self, form: SubmissionVersionForm):
238 form.instance._user = self.request.user
239 form.instance.submission = self.submission
241 def get_context_data(self, *args, **kwargs):
242 context = super().get_context_data(*args, **kwargs)
243 current_version = self.submission.current_version
244 version = current_version.number + 1 if current_version else 1
245 context["page_title"] = _("Submission files")
246 if not self.submission.is_draft:
247 context["page_title"] += f" - Version {version}"
249 submission_url = reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
250 description = "Please fill the form below to submit your files for the submission "
251 description += f"<a href='{submission_url}'><i>{self.submission.name}</i></a>.<br>"
252 context["form_description"] = [description]
254 if self.submission.is_draft:
255 stepper = get_submission_stepper(self.submission)
256 stepper.set_active_step("version")
257 context["stepper"] = stepper
258 context["form"].buttons = []
260 # # Generate breadcrumb data
261 # breadcrumb = get_submission_breadcrumb(self.submission)
262 # breadcrumb.add_item(
263 # title=_("New version"),
264 # url=reverse_lazy(
265 # "mesh:submission_version_create", kwargs={"submission_pk": self.submission.pk}
266 # ),
267 # )
268 # context["breadcrumb"] = breadcrumb
269 return context
271 def form_valid(self, form):
272 result = super().form_valid(form)
273 return result
276class SubmissionVersionUpdateView(RoleMixin, SubmittableModelFormMixin, UpdateView):
277 """
278 View for updating a submission version.
280 Submitting a version = submitting the whole submission.
281 This requires extra care about checking that all previous steps have been
282 taken when this is the first version.
283 """
285 model: type[SubmissionVersion] = SubmissionVersion
286 form_class = SubmissionVersionForm
287 template_name = "mesh/forms/form_full_page.html"
288 version: SubmissionVersion
289 restricted_roles = [Author]
290 message_on_restrict = False
291 save_submission = False
292 add_confirm_message = False
294 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
295 pk = kwargs["pk"]
296 self.version = get_object_or_404(self.model, pk=pk)
297 return not self.role_handler.check_rights("can_edit_version", self.version)
299 def get_fail_redirect_uri(self) -> str:
300 """
301 Redirects to the submission details page if the user cannot update the version.
302 """
303 return self.get_success_url()
305 def get_object(self, *args, **kwargs) -> SubmissionVersion:
306 """
307 Returns the already fetched version.
308 """
309 return self.version
311 def get_form_kwargs(self) -> dict[str, Any]:
312 kwargs = super().get_form_kwargs()
313 if self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true":
314 kwargs[SUBMIT_QUERY_PARAMETER] = True
315 return kwargs
317 def get_success_url(self) -> str:
318 if FormAction.NEXT.value in self.request.POST:
319 return self.submit_url()
320 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})
322 def submit_url(self) -> str:
323 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.version.submission.pk})
325 def form_pre_save(self, form: SubmissionVersionForm) -> None:
326 form.instance._user = self.request.user
328 def get_context_data(self, *args, **kwargs):
329 context = super().get_context_data(*args, **kwargs)
330 context["page_title"] = _(f"Submission files - Version {self.version.number}")
332 submission_url = reverse_lazy(
333 "mesh:submission_details", kwargs={"pk": self.version.submission.pk}
334 )
335 description = "Please fill the form below to submit your files for the submission "
336 description += f"<a href='{submission_url}'><i>{self.version.submission.name}</i></a>.<br>"
337 context["form_description"] = [description]
339 if self.version.submission.is_draft:
340 stepper = get_submission_stepper(self.version.submission)
341 stepper.set_active_step("version")
342 context["stepper"] = stepper
343 context["form"].buttons = []
345 # # Generate breadcrumb data
346 # breadcrumb = get_submission_breadcrumb(self.version.submission)
347 # breadcrumb.add_item(
348 # title=_("Edit version") + f" #{self.version.number}",
349 # url=reverse_lazy("mesh:submission_version_update", kwargs={"pk": self.version.pk}),
350 # )
351 # context["breadcrumb"] = breadcrumb
352 return context
354 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
355 """
356 View for updating the submission or deleting one of the associated files.
357 """
358 deletion_requested, _ = post_delete_model_file(self.version, request, self.role_handler)
360 if deletion_requested:
361 # Update the submission object.
362 self.version = get_object_or_404(self.model, pk=self.version.pk)
363 return self.get(request, *args, **kwargs)
365 return super().post(request, *args, **kwargs)
368class SubmissionResumeView(RoleMixin, View):
369 """
370 View handling the first submit of a submission. It resumes the submission process
371 at the correct edit view according to the missing data.
373 Handles the submission stepper. It redirects to the correct step according to
374 the submission status.
375 - STEP 1: Metadata
376 - STEP 2: Authors
377 - STEP 3: Files
378 - STEP 4: Confirmation
379 """
381 submission: Submission
382 restricted_roles = [Author]
384 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
385 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
386 return not self.role_handler.check_rights("can_submit_submission", self.submission)
388 def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
389 stepper = get_submission_stepper(self.submission)
391 # Redirects to the last active step
392 for i in range(len(stepper.steps)):
393 step = stepper.steps[-(i + 1)]
394 if step.href and step.can_navigate:
395 return HttpResponseRedirect(step.href)
397 return HttpResponseRedirect(reverse_lazy("mesh:submission_create"))
400class SubmissionAuthorView(RoleMixin, TemplateView):
401 """
402 View to add/remove a submission author from a given submission.
403 """
405 submission: Submission
406 template_name = "mesh/forms/submission_author.html"
407 restricted_roles = [Author]
408 init_form: SubmissionAuthorForm | None = None
409 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding"
411 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
412 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
413 return not self.role_handler.check_rights("can_edit_submission", self.submission)
415 @cached_property
416 def authors(self) -> QuerySet[SubmissionAuthor]:
417 return SubmissionAuthor.objects.filter(submission=self.submission)
419 def get_context_data(self, *args, **kwargs):
420 context = super().get_context_data(*args, **kwargs)
422 context["submission"] = self.submission
424 authors = self.authors
425 context["authors"] = [
426 {
427 "author": author,
428 "delete_form": HiddenModelChoiceForm(
429 _queryset=authors,
430 form_action=FormAction.DELETE.value,
431 initial={"instance": author},
432 ),
433 "corresponding_form": HiddenModelChoiceForm(
434 _queryset=authors,
435 form_action="_action_toggle_corresponding",
436 initial={"instance": author},
437 ),
438 "buttons": [
439 Button(
440 id=f"author-corresponding-{author.pk}",
441 title=_("Corresponding"),
442 icon_class=("fa-toggle-on" if author.corresponding else "fa-toggle-off"),
443 form=HiddenModelChoiceForm(
444 _queryset=authors, initial={"instance": author}
445 ),
446 attrs={
447 "href": [
448 reverse_lazy(
449 "mesh:submission_authors",
450 kwargs={"pk": self.submission.pk},
451 )
452 ],
453 "type": ["submit"],
454 "class": ["primary" if author.corresponding else "inactive"],
455 "name": [self._FORM_ACTION_CORRESPONDING],
456 "data-tooltip": [
457 _("Click to toggle whether the author is a corresponding contact.")
458 ],
459 },
460 ),
461 Button(
462 id=f"author-delete-{author.pk}",
463 title=_("Remove"),
464 icon_class="fa-trash",
465 form=HiddenModelChoiceForm(
466 _queryset=authors, initial={"instance": author}
467 ),
468 attrs={
469 "href": [
470 reverse_lazy(
471 "mesh:submission_authors",
472 kwargs={"pk": self.submission.pk},
473 )
474 ],
475 "type": ["submit"],
476 "class": ["button-error"],
477 "name": [FormAction.DELETE.value],
478 },
479 ),
480 ],
481 }
482 for author in authors
483 ]
485 initial = {}
486 if not authors:
487 initial.update(
488 {
489 "first_name": self.request.user.first_name, # type:ignore
490 "last_name": self.request.user.last_name, # type:ignore
491 "email": self.request.user.email, # type:ignore
492 "primary": True,
493 }
494 )
496 form = self.init_form or SubmissionAuthorForm(
497 submission=self.submission, initial=initial or None
498 )
500 form.buttons = [
501 Button(
502 id="form_save",
503 title=_("Add author"),
504 icon_class="fa-plus",
505 attrs={"type": ["submit"], "class": ["save-button"]},
506 )
507 ]
509 context["page_title"] = _("Authors")
511 if self.submission.is_draft:
512 step_id = "authors"
513 stepper = get_submission_stepper(self.submission)
514 stepper.set_active_step(step_id)
515 context["stepper"] = stepper
516 # version_step = stepper.get_step("version")
517 # if version_step and version_step.can_navigate and version_step.href:
518 # button = Button(
519 # id="next",
520 # title=_("Next"),
521 # icon_class="fa-right-long",
522 # attrs={"href": [version_step.href], "class": ["as-button"]},
523 # )
524 # context["title_buttons"] = [button]
525 #
526 # form.buttons.append(
527 # Button(
528 # id="form_next",
529 # title=_("Next"),
530 # icon_class="fa-right-long",
531 # attrs={
532 # "href": [version_step.href],
533 # "type": ["submit"],
534 # "class": ["as-button", "save-button"],
535 # },
536 # )
537 # )
539 context["form"] = form
541 # # Generate breadcrumb data
542 # breadcrumb = get_submission_breadcrumb(self.submission)
543 # breadcrumb.add_item(
544 # title=_("Authors"),
545 # url=reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk}),
546 # )
547 # context["breadcrumb"] = breadcrumb
549 return context
551 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
552 if FormAction.DELETE.value in request.POST:
553 return self.remove_author(request)
554 elif self._FORM_ACTION_CORRESPONDING in request.POST:
555 return self.toggle_primary_author(request)
557 return self.add_author(request, *args, **kwargs)
559 def add_author(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
560 """
561 Add an `SubmissionAuthor` to the submission.
562 """
563 response = HttpResponseRedirect(
564 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
565 )
566 form = SubmissionAuthorForm(request.POST, submission=self.submission)
567 if not form.is_valid():
568 self.init_form = form
569 return self.get(request, *args, **kwargs)
571 form.instance._user = self.request.user
572 form.instance.submission = self.submission
573 form.save()
575 SubmissionLog.add_message(
576 self.submission,
577 content=f"Author added: {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
578 content_en=_("Author added")
579 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
580 user=request.user,
581 significant=True,
582 )
583 messages.success(
584 request,
585 _("Author added")
586 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
587 )
589 return response
591 def remove_author(self, request: HttpRequest) -> HttpResponse:
592 """
593 Remove a `SubmissionAuthor` from the submission.
594 """
595 response = HttpResponseRedirect(
596 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
597 )
598 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
599 if not form.is_valid():
600 messages.error(request, _("Something went wrong. Please try again."))
601 return response
603 if len(self.authors) < 2:
604 messages.error(
605 request,
606 _("There must be at least 1 author attached to the submission."),
607 )
608 return response
610 author: SubmissionAuthor = form.cleaned_data["instance"]
611 author_string = str(author)
612 author.delete()
614 SubmissionLog.add_message(
615 self.submission,
616 content=f"Author removed: {author_string}",
617 content_en=_("Author added") + f": {author_string}",
618 request=request.user,
619 significant=True,
620 )
621 messages.success(request, _("The author has been removed."))
622 return response
624 def toggle_primary_author(self, request: HttpRequest) -> HttpResponse:
625 """
626 Toggle the `corresponding` boolean of a `SubmissionAuthor` from the submission.
627 """
628 response = HttpResponseRedirect(
629 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
630 )
631 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
632 if not form.is_valid():
633 messages.error(request, _("Something went wrong. Please try again."))
634 return response
636 author: SubmissionAuthor = form.cleaned_data["instance"]
637 author.corresponding = not author.corresponding
638 author.save()
639 word = "marked" if author.corresponding else "unmarked"
640 messages.success(
641 request,
642 _(f"Author {author} has been {word} as a corresponding contact."),
643 )
645 return response
648class SubmissionConfirmView(RoleMixin, FormView):
649 """
650 View to confirm the submission the current SubmissionVersion.
652 It's used both when submitting a Submission for the first time and when
653 submitting a revised SubmissionVersion.
654 """
656 template_name = "mesh/submission/version_confirm.html"
657 submission: Submission
658 form_class = SubmissionConfirmForm
660 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
661 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
663 if not self.role_handler.check_rights("can_edit_submission", self.submission):
664 return True
666 return not self.submission.is_submittable
668 def get_success_url(self):
669 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
671 def get_object(self, queryset=None) -> SubmissionVersion:
672 return self.submission.current_version # type:ignore
674 def get_context_data(self, *args, **kwargs):
675 context = super().get_context_data(*args, **kwargs)
677 context["form"].buttons = [
678 Button(
679 id="form_save",
680 title=_("Submit"),
681 icon_class="fa-check",
682 attrs={
683 "type": ["submit"],
684 "class": ["save-button", "button-highlight"],
685 },
686 )
687 ]
689 if self.submission.is_draft:
690 stepper = get_submission_stepper(self.submission)
691 stepper.set_active_step("confirm")
692 context["stepper"] = stepper
694 context["submission_proxy"] = SubmissionProxy(self.submission, self.role_handler)
696 files = []
697 files.append(
698 {
699 "file_wrapper": self.submission.current_version.main_file, # type:ignore
700 "type": "Main",
701 }
702 )
704 for file_wrapper in self.submission.current_version.additional_files.all(): # type:ignore
705 files.append({"file_wrapper": file_wrapper, "type": "Additional"})
706 context["submission_files"] = files
708 # # Breadcrumb
709 # breadcrumb = get_submission_breadcrumb(self.submission)
710 # breadcrumb.add_item(
711 # title=_("Authors"),
712 # url=reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk}),
713 # )
714 # context["breadcrumb"] = breadcrumb
716 return context
718 def form_valid(self, form: SubmissionConfirmForm) -> HttpResponse:
719 """
720 Submit the submission's current version.
721 """
722 self.submission.submit(self.request.user)
724 if self.submission.current_version.number == 1: # type:ignore
725 messages.success(
726 self.request,
727 _("Your submission has been successfully saved and confirmed."),
728 )
729 else:
730 messages.success(self.request, _("Your revisions were successfully submitted."))
732 previous_round_number = self.submission.current_version.number - 1
733 previous_round = self.submission.versions.get(number=previous_round_number)
734 previous_reviewers = [
735 review.reviewer
736 for review in previous_round.reviews.filter(state=ReviewState.SUBMITTED.value)
737 ]
738 for previous_reviewer in previous_reviewers:
739 qs = Suggestion.objects.filter(
740 submission=self.submission, suggested_user=previous_reviewer
741 )
742 if not qs.exists():
743 add_suggestion(
744 submission=self.submission,
745 suggested_user=previous_reviewer,
746 suggested_reviewer=None,
747 )
749 return HttpResponseRedirect(self.get_success_url())