Coverage for src/mesh/views/views_submission_edit.py: 26%
505 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-12-23 15:28 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-12-23 15:28 +0000
1import json
2from functools import cached_property
3from typing import Any
5from django.contrib import messages
6from django.db.models import QuerySet
7from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse
8from django.shortcuts import get_object_or_404
9from django.urls import reverse_lazy
10from django.utils.translation import gettext_lazy as _
11from django.views.generic import FormView, TemplateView, View
12from django.views.generic.edit import CreateView, DeleteView, UpdateView
13from ptf.external.fetch_metadata import fetch_article
14from ptf.model_data import create_articledata, create_contributor
15from ptf.views import ArticleEditFormWithVueAPIView
17from mesh.model.file_helpers import post_delete_model_file
18from mesh.model.roles.author import Author
19from mesh.views.forms.base_forms import FormAction, HiddenModelChoiceForm
20from mesh.views.forms.submission_forms import (
21 SubmissionAuthorForm,
22 SubmissionConfirmForm,
23 SubmissionCreateForm,
24 SubmissionDeleteForm,
25 SubmissionEditArticleMetadataForm,
26 SubmissionInfoForm,
27 SubmissionVersionForm,
28)
29from mesh.views.mixins import RoleMixin
31from ..app_settings import app_settings
32from ..model.roles.journal_manager import JournalManager
33from ..models.review_models import ReviewState
34from ..models.submission_models import (
35 Submission,
36 SubmissionAuthor,
37 SubmissionLog,
38 SubmissionMainFile,
39 SubmissionVersion,
40)
41from ..models.user_models import Suggestion
42from ..views.views_base import SUBMIT_QUERY_PARAMETER, SubmittableModelFormMixin
43from ..views.views_reviewer import add_suggestion, add_suggestion_from_person
45# from .components.breadcrumb import get_base_breadcrumb
46# from .components.breadcrumb import get_submission_breadcrumb
47from .components.button import Button
48from .components.stepper import get_submission_stepper
49from .model_proxy import SubmissionProxy
51# def submission_stepper_form_buttons() -> list[Button]:
52# return [
53# Button(
54# id="form_save",
55# title=_("Save as draft"),
56# icon_class="fa-floppy-disk",
57# attrs={"type": ["submit"], "class": ["save-button", "light"]},
58# ),
59# Button(
60# id="form_next",
61# title=_("Next"),
62# icon_class="fa-right-long",
63# attrs={"type": ["submit"], "class": ["save-button"], "name": [FormAction.NEXT.value]},
64# ),
65# ]
68class SubmissionCreateView(RoleMixin, CreateView):
69 """
70 View for creating a new submission with a preprint id.
71 """
73 model = Submission
74 form_class = SubmissionCreateForm
75 template_name = "mesh/forms/form_full_page.html"
76 restricted_roles = [Author]
78 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
79 self.submission = None
80 return not self.role_handler.check_rights("can_create_submission")
82 def get_success_url(self) -> str:
83 """
84 Redirects to the SubmissionAuthor view to add authors to the
85 newly created submission.
86 """
88 return reverse_lazy(
89 "mesh:submission_edit_article_metadata", kwargs={"pk": self.submission.pk}
90 )
92 def form_valid(self, form: SubmissionCreateForm) -> HttpResponse:
93 preprint_id = form.cleaned_data["preprint_id"]
95 title = ""
96 abstract = ""
97 article_data = None
98 if preprint_id != "":
99 article_data = fetch_article(preprint_id, with_bib=False)
101 title = article_data.title_html
102 abstract = ""
103 if len(article_data.abstracts) > 0:
104 abstract = article_data.abstracts[0]["value_html"]
106 self.submission = Submission(name=title, abstract=abstract, author_agreement=False)
107 self.submission._user = self.request.user
108 self.submission.save()
110 version = SubmissionVersion(submission=self.submission)
111 version._user = self.request.user
112 version.save()
114 if article_data is not None:
115 for contrib in article_data.contributors:
116 author = SubmissionAuthor(
117 submission=self.submission,
118 first_name=contrib["first_name"],
119 last_name=contrib["last_name"],
120 email=contrib["email"],
121 corresponding=contrib["corresponding"],
122 )
123 author.save()
125 SubmissionLog.add_message(
126 self.submission,
127 content=_("Creation of the submission"),
128 content_en="Creation of the submission",
129 user=self.request.user,
130 significant=True,
131 )
133 return HttpResponseRedirect(self.get_success_url())
135 def get_context_data(self, *args, **kwargs):
136 context = super().get_context_data(*args, **kwargs)
137 context["page_title"] = _("New submission")
139 # context["data-get-url"] = reverse_lazy("mesh:submission_create_vuejs")
141 # Stepper
142 step_id = "preprint"
143 stepper = get_submission_stepper(None)
144 stepper.set_active_step(step_id)
145 context["stepper"] = stepper
147 # Form buttons
148 # No Save/Next button in the form, they are put in the stepper
149 context["form"].buttons = [] # = submission_stepper_form_buttons()
151 descriptions = [
152 "You are about to start the submit process.",
153 'Please read our guidelines <a href="">here</a>',
154 "If you have a preprint, you can enter its id below. Leave the field blank otherwise.",
155 ]
156 context["form_description"] = descriptions
158 # # Generate breadcrumb data
159 # breadcrumb = get_base_breadcrumb()
160 # breadcrumb.add_item(title=_("New submission"), url=reverse_lazy("mesh:submission_create"))
161 # context["breadcrumb"] = breadcrumb
162 return context
165class SubmissionPreprintUpdateView(RoleMixin, UpdateView):
166 """
167 View for updating a submission with a new preprint id.
168 """
170 model = Submission
171 form_class = SubmissionCreateForm
172 template_name = "mesh/forms/form_full_page.html"
173 restricted_roles = [Author]
175 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
176 pk = kwargs["pk"]
177 self.submission = get_object_or_404(self.model, pk=pk)
179 return not self.role_handler.check_rights("can_edit_submission", self.submission)
181 def get_object(self, *args, **kwargs) -> Submission:
182 """
183 Override `get_object` built-in method to avoid an additional look-up when
184 we already have the object loaded.
185 """
186 return self.submission
188 def get_success_url(self) -> str:
189 """
190 Redirects to the SubmissionAuthor view to add authors to the
191 newly created submission.
192 """
194 return reverse_lazy("mesh:submission_edit_article_metadata", kwargs={"pk": self.object.pk})
196 def form_valid(self, form: SubmissionCreateForm) -> HttpResponse:
197 preprint_id = form.cleaned_data["preprint_id"]
199 if preprint_id != "":
200 article_data = fetch_article(preprint_id, with_bib=False)
202 title = article_data.title_html
203 self.submission.name = title
205 if len(article_data.abstracts) > 0:
206 abstract = article_data.abstracts[0]["value_tex"]
207 self.submission.abstract = abstract
209 self.submission._user = self.request.user
210 self.submission.save()
212 current_version = self.submission.current_version
213 current_version._user = self.request.user
214 current_version.save()
216 SubmissionAuthor.objects.filter(submission=self.submission).delete()
218 for contrib in article_data.contributors:
219 author = SubmissionAuthor(
220 submission=self.submission,
221 first_name=contrib["first_name"],
222 last_name=contrib["last_name"],
223 email=contrib["email"],
224 corresponding=contrib["corresponding"],
225 )
226 author.save()
228 SubmissionLog.add_message(
229 self.submission,
230 content=_("Update of the submission with a preprint id"),
231 content_en="Update of the submission with a preprint id",
232 user=self.request.user,
233 significant=True,
234 )
236 return HttpResponseRedirect(self.get_success_url())
238 def get_context_data(self, *args, **kwargs):
239 context = super().get_context_data(*args, **kwargs)
240 context["page_title"] = _("Update submission preprint id")
242 # context["data-get-url"] = reverse_lazy("mesh:submission_create_vuejs")
244 # Stepper
245 step_id = "preprint"
246 stepper = get_submission_stepper(self.submission)
247 stepper.set_active_step(step_id)
248 context["stepper"] = stepper
250 # Form buttons
251 # No Save/Next button in the form, they are put in the stepper
252 context["form"].buttons = [] # = submission_stepper_form_buttons()
254 descriptions = [
255 "You are about to start the submit process.",
256 'Please read our guidelines <a href="">here</a>',
257 "If you have a preprint, you can enter its id below. Leave the field blank otherwise.",
258 ]
259 context["form_description"] = descriptions
261 # # Generate breadcrumb data
262 # breadcrumb = get_base_breadcrumb()
263 # breadcrumb.add_item(title=_("New submission"), url=reverse_lazy("mesh:submission_create"))
264 # context["breadcrumb"] = breadcrumb
265 return context
268# class SubmissionCreateVuejsAPIView(RoleMixin, ArticleEditFormWithVueAPIView):
269# restricted_roles = [Author]
270#
271# def __init__(self, **kwargs):
272# super().__init__(**kwargs)
273# self.fields_to_update = ["contributors", "abstracts", "titles", "pdf"]
274# self.editorial_tools = ["light_authors", "inline_ckeditor"]
275#
276# self.submission = None
277# self.version = None
278# self.submission_main_file = None
279#
280# def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
281# return not self.role_handler.check_rights("can_create_submission")
282#
283# def handle_pdf_for_vuejs3(self, data, data_article, method=""):
284# if method == "get":
285# if self.submission_main_file is not None:
286# data["article"]["pdf_name"] = self.submission_main_file.file.name
287# else:
288# data["article"]["pdf_name"] = "No file uploaded"
289#
290# # elif method == "post":
291# # if self.submission_main_file is not None and "pdf" in self.request.FILES:
292# # self.submission_main_file = SubmissionMainFile.objects.create(
293# # file=self.request.FILES["pdf"], attached_to=self.version
294# # )
295#
296# def post(self, request, *args, **kwargs):
297# body_unicode = request.POST.get("data", "")
298# data = json.loads(body_unicode)
299#
300# if (
301# data["title_html"][0]["value"] == ""
302# or data["abstracts"][0]["value_html"] == ""
303# or len(data["contributors"]) == 0
304# or ("pdf" not in self.request.FILES and data["pdf"]["pdf_name"] == "No file uploaded")
305# ):
306# return JsonResponse({"status_code": 400})
307#
308# data["colid"] = app_settings.COLID
309#
310# article_data = self.convert_data_from_vuejs3(data)
311# title = article_data.titles[0]["title_html"]
312# abstract = article_data.abstracts[0]["value_html"]
313#
314# submission = Submission(name=title, abstract=abstract, author_agreement=False)
315# submission._user = request.user
316# submission.save()
317#
318# version = SubmissionVersion(submission=submission)
319# version._user = request.user
320# version.save()
321#
322# submission_main_file = SubmissionMainFile(
323# attached_to=version, file=self.request.FILES["pdf"]
324# )
325# submission_main_file.save()
326#
327# for contrib in article_data.contributors:
328# author = SubmissionAuthor(
329# submission=submission,
330# first_name=contrib["first_name"],
331# last_name=contrib["last_name"],
332# email=contrib["email"],
333# corresponding=contrib["corresponding"],
334# )
335# author.save()
336#
337# SubmissionLog.add_message(
338# submission,
339# content=_("Creation of the submission"),
340# content_en="Creation of the submission",
341# user=request.user,
342# significant=True,
343# )
344#
345# data["redirect"] = {
346# "redirect": True,
347# "url": reverse_lazy("mesh:submission_info_update", kwargs={"pk": submission.pk}),
348# }
349#
350# return JsonResponse(data)
353class SubmissionEditArticleMetadataVuejsAPIView(RoleMixin, ArticleEditFormWithVueAPIView):
354 def __init__(self, **kwargs):
355 super().__init__(**kwargs)
356 self.fields_to_update = ["contributors", "abstracts", "titles", "pdf"]
357 self.editorial_tools = ["light_authors", "inline_ckeditor"]
358 self.mandatory_fields = ["contributors", "abstract", "title", "pdf"]
360 self.submission = None
361 self.version = None
362 self.submission_main_file = None
364 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
365 pk = kwargs["pk"]
366 self.submission = get_object_or_404(Submission, pk=pk)
368 return not self.role_handler.check_rights("can_edit_submission", self.submission)
370 def get(self, request, *args, **kwargs):
371 colid = app_settings.COLID
372 data_article = create_articledata()
373 data = self.convert_data_for_vuejs3(data_article, colid=colid)
374 data["editorial_tools"] = self.editorial_tools
376 def obj_to_dict(obj):
377 return obj.__dict__
379 data["article"]["colid"] = colid
380 dump = json.dumps(data, default=obj_to_dict)
381 return HttpResponse(dump, content_type="application/json")
383 def handle_pdf_for_vuejs3(self, data, data_article, method=""):
384 if method == "get":
385 submission_main_file = getattr(self.submission.current_version, "main_file", None)
386 if submission_main_file is not None:
387 data["article"]["pdf_name"] = submission_main_file.file.name
388 else:
389 data["article"]["pdf_name"] = "No file uploaded"
391 elif method == "post" and "pdf" in self.request.FILES:
392 submission_main_file = getattr(self.submission.current_version, "main_file", None)
393 if submission_main_file is not None:
394 submission_main_file.delete()
396 submission_main_file = SubmissionMainFile(
397 attached_to=self.submission.current_version, file=self.request.FILES["pdf"]
398 )
399 submission_main_file.save()
401 def handle_titles_for_vuejs3(self, data, data_article, method=""):
402 if method == "get":
403 data["article"]["titles"] = [
404 {"lang": data_article.lang, "value": self.submission.name}
405 ]
406 else:
407 super().handle_titles_for_vuejs3(data, data_article, method)
409 def handle_contributors_for_vuejs3(self, data, data_article, method=""):
410 if method == "get":
411 contributors = []
412 for author in self.submission.authors.all():
413 contrib = create_contributor()
414 contrib["first_name"] = author.first_name
415 contrib["last_name"] = author.last_name
416 contrib["email"] = author.email
417 contrib["corresponding"] = author.corresponding
418 contributors.append(contrib)
419 data["article"]["contributors"] = contributors
420 else:
421 super().handle_contributors_for_vuejs3(data, data_article, method)
423 def handle_abstracts_for_vuejs3(self, data, data_article, method=""):
424 if method == "get":
425 data["article"]["abstracts"] = [{"lang": "en", "value": self.submission.abstract}]
426 else:
427 super().handle_abstracts_for_vuejs3(data, data_article, method)
429 def post(self, request, *args, **kwargs):
430 body_unicode = request.POST.get("data", "")
431 data = json.loads(body_unicode)
433 if (
434 data["title_html"][0]["value"] == ""
435 or data["abstracts"][0]["value_html"] == ""
436 or len(data["contributors"]) == 0
437 or ("pdf" not in self.request.FILES and data["pdf"]["pdf_name"] == "No file uploaded")
438 ):
439 return JsonResponse({"status_code": 400})
441 data["colid"] = app_settings.COLID
443 article_data = self.convert_data_from_vuejs3(data)
444 title = article_data.titles[0]["title_html"]
445 abstract = article_data.abstracts[0]["value_html"]
447 self.submission.name = title
448 self.submission.abstract = abstract
449 self.submission._user = request.user
450 self.submission.save()
452 current_version = self.submission.current_version
453 current_version._user = request.user
454 current_version.save()
456 SubmissionAuthor.objects.filter(submission=self.submission).delete()
458 for contrib in article_data.contributors:
459 author = SubmissionAuthor(
460 submission=self.submission,
461 first_name=contrib["first_name"],
462 last_name=contrib["last_name"],
463 email=contrib["email"],
464 corresponding=contrib["corresponding"],
465 )
466 author.save()
468 data["redirect"] = {
469 "redirect": True,
470 "url": reverse_lazy("mesh:submission_info_update", kwargs={"pk": self.submission.pk}),
471 }
473 return JsonResponse(data)
476class SubmissionRedirectFromVue(TemplateView):
477 template_name = "mesh/forms/form_full_page_redirect.html"
479 def get_context_data(self, **kwargs):
480 context = super().get_context_data(**kwargs)
481 context["message"] = (
482 "La sauvegarde dans Vuejs puis la redirection sur une autre page fonctionne"
483 )
484 return context
486 def get(self, request, *args, **kwargs):
487 """
488 Override the get method to set the form action to "create".
489 """
491 messages.success(self.request, _("New submission successfully created."))
493 return super().get(request, *args, **kwargs)
496class SubmissionInfoView(RoleMixin, UpdateView):
497 """
498 View for updating the submission info.
499 """
501 model: type[Submission] = Submission
502 form_class = SubmissionInfoForm
503 template_name = "mesh/forms/submission_info.html"
504 submission: Submission
505 restricted_roles = [Author]
506 message_on_restrict = False
508 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
509 pk = kwargs["pk"]
510 self.submission = get_object_or_404(self.model, pk=pk)
512 return not self.role_handler.check_rights("can_edit_submission", self.submission)
514 def get_success_url(self) -> str:
515 # if FormAction.NEXT.value in self.request.POST:
516 # return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
518 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk})
520 def get_fail_redirect_uri(self) -> str:
521 """
522 Return to the submission details page if the user cannot update the submission.
523 """
524 return self.get_success_url()
526 def get_object(self, *args, **kwargs) -> Submission:
527 """
528 Override `get_object` built-in method to avoid an additional look-up when
529 we already have the object loaded.
530 """
531 return self.submission
533 def get_context_data(self, *args, **kwargs):
534 context = super().get_context_data(*args, **kwargs)
535 context["page_title"] = _("Edit submission info")
537 if self.submission.is_draft:
538 step_id = "info"
539 stepper = get_submission_stepper(self.submission)
540 stepper.set_active_step(step_id)
541 context["stepper"] = stepper
543 # No Save/Next button in the form, they are put in the stepper
544 context["form"].buttons = [] # = submission_stepper_form_buttons()
546 suggestions = self.submission.suggestions_for_reviewer.all()
547 for suggestion in suggestions:
548 reviewer = (
549 suggestion.suggested_reviewer
550 if suggestion.suggested_reviewer is not None
551 else suggestion.suggested_user
552 )
553 if suggestion.suggest_to_avoid:
554 context["avoid_reviewer"] = reviewer
555 else:
556 context["suggested_reviewer"] = reviewer
558 return context
560 def form_valid(self, form):
561 form.instance._user = self.request.user
563 Suggestion.objects.filter(submission=self.submission).delete()
565 for prefix in ["reviewer", "avoid"]:
566 person_info = {
567 "first_name": form.data.get(f"{prefix}_first_name", ""),
568 "last_name": form.data.get(f"{prefix}_last_name", ""),
569 "email": form.data.get(f"{prefix}_email", ""),
570 }
572 if person_info["email"] != "":
573 add_suggestion_from_person(
574 self.submission, person_info, suggest_to_avoid=prefix == "avoid"
575 )
577 return super().form_valid(form)
580class SubmissionEditArticleMetadataView(RoleMixin, UpdateView):
581 """
582 View for updating a submission (only the metadata).
583 Only the GET is used to display the form (with VueJS)
584 The POST is handled by SubmissionEditArticleMetadataVuejsAPIView
585 """
587 model: type[Submission] = Submission
588 form_class = SubmissionEditArticleMetadataForm
589 template_name = "mesh/forms/form_full_page.html"
590 submission: Submission
591 restricted_roles = [Author]
592 message_on_restrict = False
594 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
595 pk = kwargs["pk"]
596 self.submission = get_object_or_404(self.model, pk=pk)
598 return not self.role_handler.check_rights("can_edit_submission", self.submission)
600 # def get_fail_redirect_uri(self) -> str:
601 # """
602 # Return to the submission details page if the user cannot update the submission.
603 # Not used with VueJS
604 # """
605 # return self.get_success_url()
607 def get_object(self, *args, **kwargs) -> Submission:
608 """
609 Override `get_object` built-in method to avoid an additional look-up when
610 we already have the object loaded.
611 """
612 return self.submission
614 def get_context_data(self, *args, **kwargs):
615 context = super().get_context_data(*args, **kwargs)
616 context["page_title"] = _("Edit article metadata")
618 # data-get-url is used by VueJS to know the GET url to call
619 context["data_get_url"] = reverse_lazy(
620 "mesh:submission_update_vuejs",
621 kwargs={"pk": self.submission.pk},
622 )
624 if self.submission.is_draft:
625 step_id = "metadata"
626 stepper = get_submission_stepper(self.submission)
627 stepper.set_active_step(step_id)
628 context["stepper"] = stepper
630 # No Save/Next button in the form, they are put in the stepper
631 context["form"].buttons = [] # = submission_stepper_form_buttons()
633 # Generate breadcrumb data
634 # breadcrumb = get_submission_breadcrumb(self.submission)
635 # breadcrumb.add_item(
636 # title=_("Edit metadata"),
637 # url=reverse_lazy("mesh:submission_edit_article_metadata", kwargs={"pk": self.submission.pk}),
638 # )
639 # context["breadcrumb"] = breadcrumb
640 return context
642 def get_success_url(self) -> str:
643 if FormAction.NEXT.value in self.request.POST:
644 # No POST with VueJS. We shouldn't reach this clause
645 return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
646 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
648 # def form_valid(self, form: SubmissionUpdateForm) -> HttpResponse:
649 # form.instance._user = self.request.user
650 # if not form.changed_data:
651 # form.instance.do_not_update = True
652 #
653 # response = super().form_valid(form)
654 #
655 # if form.has_changed():
656 # SubmissionLog.add_message(
657 # self.submission,
658 # content=_("Modification of the submission's metadata"),
659 # content_en="Modification of the submission's metadata",
660 # user=self.request.user,
661 # )
662 # return response
665class SubmissionVersionCreateView(RoleMixin, SubmittableModelFormMixin, CreateView):
666 """
667 View for creating a new submission version.
668 """
670 model = SubmissionVersion
671 form_class = SubmissionVersionForm
672 template_name = "mesh/forms/form_full_page.html"
673 submission: Submission
674 restricted_roles = [Author]
675 save_submission = False
676 add_confirm_message = False
678 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
679 submission_pk = kwargs["submission_pk"]
680 self.submission = get_object_or_404(Submission, pk=submission_pk)
682 return not self.role_handler.check_rights("can_create_version", self.submission)
684 def get_success_url(self) -> str:
685 if FormAction.NEXT.value in self.request.POST:
686 # No POST with VueJS. We shouldn't reach this clause
687 return self.submit_url()
688 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
690 def submit_url(self) -> str:
691 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk})
693 # def form_pre_save(self, form: SubmissionVersionForm):
694 # Not used with VueJS
695 # form.instance._user = self.request.user
696 # form.instance.submission = self.submission
698 def get_context_data(self, *args, **kwargs):
699 context = super().get_context_data(*args, **kwargs)
700 current_version = self.submission.current_version
701 version = current_version.number + 1 if current_version else 1
702 context["page_title"] = _("Submission files")
703 if not self.submission.is_draft:
704 context["page_title"] += f" - Version {version}"
706 submission_url = reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
707 description = "Please fill the form below to submit your files for the submission "
708 description += f"<a href='{submission_url}'><i>{self.submission.name}</i></a>.<br>"
709 context["form_description"] = [description]
711 if self.submission.is_draft:
712 stepper = get_submission_stepper(self.submission)
713 stepper.set_active_step("version")
714 context["stepper"] = stepper
715 context["form"].buttons = []
717 # # Generate breadcrumb data
718 # breadcrumb = get_submission_breadcrumb(self.submission)
719 # breadcrumb.add_item(
720 # title=_("New version"),
721 # url=reverse_lazy(
722 # "mesh:submission_version_create", kwargs={"submission_pk": self.submission.pk}
723 # ),
724 # )
725 # context["breadcrumb"] = breadcrumb
726 return context
728 # def form_valid(self, form):
729 # result = super().form_valid(form)
730 # return result
733class SubmissionVersionUpdateView(RoleMixin, SubmittableModelFormMixin, UpdateView):
734 """
735 View for updating a submission version.
737 Submitting a version = submitting the whole submission.
738 This requires extra care about checking that all previous steps have been
739 taken when this is the first version.
740 """
742 model: type[SubmissionVersion] = SubmissionVersion
743 form_class = SubmissionVersionForm
744 template_name = "mesh/forms/form_full_page.html"
745 version: SubmissionVersion
746 restricted_roles = [Author]
747 message_on_restrict = False
748 save_submission = False
749 add_confirm_message = False
751 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
752 pk = kwargs["pk"]
753 self.version = get_object_or_404(self.model, pk=pk)
754 return not self.role_handler.check_rights("can_edit_version", self.version)
756 def get_fail_redirect_uri(self) -> str:
757 """
758 Redirects to the submission details page if the user cannot update the version.
759 """
760 return self.get_success_url()
762 def get_object(self, *args, **kwargs) -> SubmissionVersion:
763 """
764 Returns the already fetched version.
765 """
766 return self.version
768 def get_form_kwargs(self) -> dict[str, Any]:
769 kwargs = super().get_form_kwargs()
770 if self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true":
771 kwargs[SUBMIT_QUERY_PARAMETER] = True
772 return kwargs
774 def get_success_url(self) -> str:
775 if FormAction.NEXT.value in self.request.POST:
776 return self.submit_url()
777 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})
779 def submit_url(self) -> str:
780 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.version.submission.pk})
782 def form_pre_save(self, form: SubmissionVersionForm) -> None:
783 form.instance._user = self.request.user
785 def get_context_data(self, *args, **kwargs):
786 context = super().get_context_data(*args, **kwargs)
787 context["page_title"] = _(f"Submission files - Version {self.version.number}")
789 submission_url = reverse_lazy(
790 "mesh:submission_details", kwargs={"pk": self.version.submission.pk}
791 )
792 description = "Please fill the form below to submit your files for the submission "
793 description += f"<a href='{submission_url}'><i>{self.version.submission.name}</i></a>.<br>"
794 context["form_description"] = [description]
796 if self.version.submission.is_draft:
797 stepper = get_submission_stepper(self.version.submission)
798 stepper.set_active_step("version")
799 context["stepper"] = stepper
800 context["form"].buttons = []
802 # # Generate breadcrumb data
803 # breadcrumb = get_submission_breadcrumb(self.version.submission)
804 # breadcrumb.add_item(
805 # title=_("Edit version") + f" #{self.version.number}",
806 # url=reverse_lazy("mesh:submission_version_update", kwargs={"pk": self.version.pk}),
807 # )
808 # context["breadcrumb"] = breadcrumb
809 return context
811 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
812 """
813 View for updating the submission or deleting one of the associated files.
814 """
815 deletion_requested, _ = post_delete_model_file(self.version, request, self.role_handler)
817 if deletion_requested:
818 # Update the submission object.
819 self.version = get_object_or_404(self.model, pk=self.version.pk)
820 return self.get(request, *args, **kwargs)
822 return super().post(request, *args, **kwargs)
825class SubmissionResumeView(RoleMixin, View):
826 """
827 View handling the first submit of a submission. It resumes the submission process
828 at the correct edit view according to the missing data.
830 Handles the submission stepper. It redirects to the correct step according to
831 the submission status.
832 - STEP 1: Preprint id
833 - STEP 2: Article Metadata
834 - STEP 3: Submission Info
835 - STEP 4: Confirmation
836 """
838 submission: Submission
839 restricted_roles = [Author]
841 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
842 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
843 return not self.role_handler.check_rights("can_submit_submission", self.submission)
845 def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
846 stepper = get_submission_stepper(self.submission)
848 # Redirects to the last active step
849 for i in range(len(stepper.steps)):
850 step = stepper.steps[-(i + 1)]
851 if step.href and step.can_navigate:
852 return HttpResponseRedirect(step.href)
854 return HttpResponseRedirect(reverse_lazy("mesh:submission_create"))
857class SubmissionAuthorView(RoleMixin, TemplateView):
858 """
859 View to add/remove a submission author from a given submission.
860 """
862 submission: Submission
863 template_name = "mesh/forms/submission_author.html"
864 restricted_roles = [Author]
865 init_form: SubmissionAuthorForm | None = None
866 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding"
868 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
869 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
870 return not self.role_handler.check_rights("can_edit_submission", self.submission)
872 @cached_property
873 def authors(self) -> QuerySet[SubmissionAuthor]:
874 return SubmissionAuthor.objects.filter(submission=self.submission)
876 def get_context_data(self, *args, **kwargs):
877 context = super().get_context_data(*args, **kwargs)
879 context["submission"] = self.submission
881 authors = self.authors
882 context["authors"] = [
883 {
884 "author": author,
885 "delete_form": HiddenModelChoiceForm(
886 _queryset=authors,
887 form_action=FormAction.DELETE.value,
888 initial={"instance": author},
889 ),
890 "corresponding_form": HiddenModelChoiceForm(
891 _queryset=authors,
892 form_action="_action_toggle_corresponding",
893 initial={"instance": author},
894 ),
895 "buttons": [
896 Button(
897 id=f"author-corresponding-{author.pk}",
898 title=_("Corresponding"),
899 icon_class=("fa-toggle-on" if author.corresponding else "fa-toggle-off"),
900 form=HiddenModelChoiceForm(
901 _queryset=authors, initial={"instance": author}
902 ),
903 attrs={
904 "href": [
905 reverse_lazy(
906 "mesh:submission_authors",
907 kwargs={"pk": self.submission.pk},
908 )
909 ],
910 "type": ["submit"],
911 "class": ["primary" if author.corresponding else "inactive"],
912 "name": [self._FORM_ACTION_CORRESPONDING],
913 "data-tooltip": [
914 _("Click to toggle whether the author is a corresponding contact.")
915 ],
916 },
917 ),
918 Button(
919 id=f"author-delete-{author.pk}",
920 title=_("Remove"),
921 icon_class="fa-trash",
922 form=HiddenModelChoiceForm(
923 _queryset=authors, initial={"instance": author}
924 ),
925 attrs={
926 "href": [
927 reverse_lazy(
928 "mesh:submission_authors",
929 kwargs={"pk": self.submission.pk},
930 )
931 ],
932 "type": ["submit"],
933 "class": ["button-error"],
934 "name": [FormAction.DELETE.value],
935 },
936 ),
937 ],
938 }
939 for author in authors
940 ]
942 initial = {}
943 if not authors:
944 initial.update(
945 {
946 "first_name": self.request.user.first_name, # type:ignore
947 "last_name": self.request.user.last_name, # type:ignore
948 "email": self.request.user.email, # type:ignore
949 "primary": True,
950 }
951 )
953 form = self.init_form or SubmissionAuthorForm(
954 submission=self.submission, initial=initial or None
955 )
957 form.buttons = [
958 Button(
959 id="form_save",
960 title=_("Add author"),
961 icon_class="fa-plus",
962 attrs={"type": ["submit"], "class": ["save-button"]},
963 )
964 ]
966 context["page_title"] = _("Authors")
968 if self.submission.is_draft:
969 step_id = "authors"
970 stepper = get_submission_stepper(self.submission)
971 stepper.set_active_step(step_id)
972 context["stepper"] = stepper
973 # version_step = stepper.get_step("version")
974 # if version_step and version_step.can_navigate and version_step.href:
975 # button = Button(
976 # id="next",
977 # title=_("Next"),
978 # icon_class="fa-right-long",
979 # attrs={"href": [version_step.href], "class": ["as-button"]},
980 # )
981 # context["title_buttons"] = [button]
982 #
983 # form.buttons.append(
984 # Button(
985 # id="form_next",
986 # title=_("Next"),
987 # icon_class="fa-right-long",
988 # attrs={
989 # "href": [version_step.href],
990 # "type": ["submit"],
991 # "class": ["as-button", "save-button"],
992 # },
993 # )
994 # )
996 context["form"] = form
998 # # Generate breadcrumb data
999 # breadcrumb = get_submission_breadcrumb(self.submission)
1000 # breadcrumb.add_item(
1001 # title=_("Authors"),
1002 # url=reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk}),
1003 # )
1004 # context["breadcrumb"] = breadcrumb
1006 return context
1008 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
1009 if FormAction.DELETE.value in request.POST:
1010 return self.remove_author(request)
1011 elif self._FORM_ACTION_CORRESPONDING in request.POST:
1012 return self.toggle_primary_author(request)
1014 return self.add_author(request, *args, **kwargs)
1016 def add_author(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
1017 """
1018 Add an `SubmissionAuthor` to the submission.
1019 """
1020 response = HttpResponseRedirect(
1021 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1022 )
1023 form = SubmissionAuthorForm(request.POST, submission=self.submission)
1024 if not form.is_valid():
1025 self.init_form = form
1026 return self.get(request, *args, **kwargs)
1028 form.instance._user = self.request.user
1029 form.instance.submission = self.submission
1030 form.save()
1032 SubmissionLog.add_message(
1033 self.submission,
1034 content=f"Author added: {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1035 content_en=_("Author added")
1036 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1037 user=request.user,
1038 significant=True,
1039 )
1040 messages.success(
1041 request,
1042 _("Author added")
1043 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1044 )
1046 return response
1048 def remove_author(self, request: HttpRequest) -> HttpResponse:
1049 """
1050 Remove a `SubmissionAuthor` from the submission.
1051 """
1052 response = HttpResponseRedirect(
1053 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1054 )
1055 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
1056 if not form.is_valid():
1057 messages.error(request, _("Something went wrong. Please try again."))
1058 return response
1060 if len(self.authors) < 2:
1061 messages.error(
1062 request,
1063 _("There must be at least 1 author attached to the submission."),
1064 )
1065 return response
1067 author: SubmissionAuthor = form.cleaned_data["instance"]
1068 author_string = str(author)
1069 author.delete()
1071 SubmissionLog.add_message(
1072 self.submission,
1073 content=f"Author removed: {author_string}",
1074 content_en=_("Author added") + f": {author_string}",
1075 request=request.user,
1076 significant=True,
1077 )
1078 messages.success(request, _("The author has been removed."))
1079 return response
1081 def toggle_primary_author(self, request: HttpRequest) -> HttpResponse:
1082 """
1083 Toggle the `corresponding` boolean of a `SubmissionAuthor` from the submission.
1084 """
1085 response = HttpResponseRedirect(
1086 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1087 )
1088 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
1089 if not form.is_valid():
1090 messages.error(request, _("Something went wrong. Please try again."))
1091 return response
1093 author: SubmissionAuthor = form.cleaned_data["instance"]
1094 author.corresponding = not author.corresponding
1095 author.save()
1096 word = "marked" if author.corresponding else "unmarked"
1097 messages.success(
1098 request,
1099 _(f"Author {author} has been {word} as a corresponding contact."),
1100 )
1102 return response
1105class SubmissionConfirmView(RoleMixin, FormView):
1106 """
1107 View to confirm the submission the current SubmissionVersion.
1109 It's used both when submitting a Submission for the first time and when
1110 submitting a revised SubmissionVersion.
1111 """
1113 template_name = "mesh/submission/version_confirm.html"
1114 submission: Submission
1115 form_class = SubmissionConfirmForm
1117 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
1118 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
1120 if not self.role_handler.check_rights("can_edit_submission", self.submission):
1121 return True
1123 return not self.submission.is_submittable
1125 def get_success_url(self):
1126 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
1128 def get_object(self, queryset=None) -> SubmissionVersion:
1129 return self.submission.current_version # type:ignore
1131 def get_context_data(self, *args, **kwargs):
1132 context = super().get_context_data(*args, **kwargs)
1134 context["form"].buttons = [
1135 Button(
1136 id="form_save",
1137 title=_("Submit"),
1138 icon_class="fa-check",
1139 attrs={
1140 "type": ["submit"],
1141 "class": ["save-button", "button-highlight"],
1142 },
1143 )
1144 ]
1146 if self.submission.is_draft:
1147 stepper = get_submission_stepper(self.submission)
1148 stepper.set_active_step("confirm")
1149 context["stepper"] = stepper
1151 context["submission_proxy"] = SubmissionProxy(self.submission, self.role_handler)
1153 files = []
1154 files.append(
1155 {
1156 "file_wrapper": self.submission.current_version.main_file, # type:ignore
1157 "type": "Main",
1158 }
1159 )
1161 for file_wrapper in self.submission.current_version.additional_files.all(): # type:ignore
1162 files.append({"file_wrapper": file_wrapper, "type": "Additional"})
1163 context["submission_files"] = files
1165 # # Breadcrumb
1166 # breadcrumb = get_submission_breadcrumb(self.submission)
1167 # breadcrumb.add_item(
1168 # title=_("Authors"),
1169 # url=reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk}),
1170 # )
1171 # context["breadcrumb"] = breadcrumb
1173 return context
1175 def form_valid(self, form: SubmissionConfirmForm) -> HttpResponse:
1176 """
1177 Submit the submission's current version.
1178 """
1179 self.submission.submit(self.request.user)
1181 if self.submission.current_version.number == 1: # type:ignore
1182 messages.success(
1183 self.request,
1184 _("Your submission has been successfully saved and confirmed."),
1185 )
1186 else:
1187 messages.success(self.request, _("Your revisions were successfully submitted."))
1189 previous_round_number = self.submission.current_version.number - 1
1190 if previous_round_number > 0:
1191 previous_round = self.submission.versions.get(number=previous_round_number)
1192 previous_reviewers = [
1193 review.reviewer
1194 for review in previous_round.reviews.filter(state=ReviewState.SUBMITTED.value)
1195 ]
1196 for previous_reviewer in previous_reviewers:
1197 qs = Suggestion.objects.filter(
1198 submission=self.submission, suggested_user=previous_reviewer
1199 )
1200 if not qs.exists():
1201 add_suggestion(
1202 submission=self.submission,
1203 suggested_user=previous_reviewer,
1204 suggested_reviewer=None,
1205 )
1207 return HttpResponseRedirect(self.get_success_url())
1210class SubmissionDeleteView(RoleMixin, DeleteView):
1211 model: type[Submission] = Submission
1212 submission: Submission
1213 form_class = SubmissionDeleteForm
1214 template_name = "mesh/forms/submission_delete.html"
1215 restricted_roles = [JournalManager, Author]
1216 message_on_restrict = False
1218 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
1219 pk = kwargs["pk"]
1220 self.submission = get_object_or_404(self.model, pk=pk)
1222 return not self.role_handler.check_rights("can_edit_submission", self.submission)
1224 def get_object(self, *args, **kwargs) -> Submission:
1225 """
1226 Override `get_object` built-in method to avoid an additional look-up when
1227 we already have the object loaded.
1228 """
1229 return self.submission
1231 def get_success_url(self):
1232 return reverse_lazy("mesh:submission_list")
1234 def get_context_data(self, *args, **kwargs):
1235 context = super().get_context_data(*args, **kwargs)
1237 context["submission"] = self.submission
1238 context["form"].buttons = [
1239 Button(
1240 id="form_save",
1241 title=_("Delete"),
1242 icon_class="fa-trash",
1243 attrs={
1244 "type": ["submit"],
1245 "class": ["save-button", "button-highlight"],
1246 },
1247 )
1248 ]
1250 return context