Coverage for src/mesh/views/views_submission_edit.py: 26%
504 statements
« prev ^ index » next coverage.py v7.9.0, created at 2025-09-10 11:20 +0000
« prev ^ index » next coverage.py v7.9.0, created at 2025-09-10 11:20 +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 matching import crossref
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 = crossref.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 = crossref.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"]
359 self.submission = None
360 self.version = None
361 self.submission_main_file = None
363 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
364 pk = kwargs["pk"]
365 self.submission = get_object_or_404(Submission, pk=pk)
367 return not self.role_handler.check_rights("can_edit_submission", self.submission)
369 def get(self, request, *args, **kwargs):
370 colid = app_settings.COLID
371 data_article = create_articledata()
372 data = self.convert_data_for_vuejs3(data_article, colid=colid)
373 data["editorial_tools"] = self.editorial_tools
375 def obj_to_dict(obj):
376 return obj.__dict__
378 data["article"]["colid"] = colid
379 dump = json.dumps(data, default=obj_to_dict)
380 return HttpResponse(dump, content_type="application/json")
382 def handle_pdf_for_vuejs3(self, data, data_article, method=""):
383 if method == "get":
384 submission_main_file = getattr(self.submission.current_version, "main_file", None)
385 if submission_main_file is not None:
386 data["article"]["pdf_name"] = submission_main_file.file.name
387 else:
388 data["article"]["pdf_name"] = "No file uploaded"
390 elif method == "post" and "pdf" in self.request.FILES:
391 submission_main_file = getattr(self.submission.current_version, "main_file", None)
392 if submission_main_file is not None:
393 submission_main_file.delete()
395 submission_main_file = SubmissionMainFile(
396 attached_to=self.submission.current_version, file=self.request.FILES["pdf"]
397 )
398 submission_main_file.save()
400 def handle_titles_for_vuejs3(self, data, data_article, method=""):
401 if method == "get":
402 data["article"]["titles"] = [
403 {"lang": data_article.lang, "value": self.submission.name}
404 ]
405 else:
406 super().handle_titles_for_vuejs3(data, data_article, method)
408 def handle_contributors_for_vuejs3(self, data, data_article, method=""):
409 if method == "get":
410 contributors = []
411 for author in self.submission.authors.all():
412 contrib = create_contributor()
413 contrib["first_name"] = author.first_name
414 contrib["last_name"] = author.last_name
415 contrib["email"] = author.email
416 contrib["corresponding"] = author.corresponding
417 contributors.append(contrib)
418 data["article"]["contributors"] = contributors
419 else:
420 super().handle_contributors_for_vuejs3(data, data_article, method)
422 def handle_abstracts_for_vuejs3(self, data, data_article, method=""):
423 if method == "get":
424 data["article"]["abstracts"] = [{"lang": "en", "value": self.submission.abstract}]
425 else:
426 super().handle_abstracts_for_vuejs3(data, data_article, method)
428 def post(self, request, *args, **kwargs):
429 body_unicode = request.POST.get("data", "")
430 data = json.loads(body_unicode)
432 if (
433 data["title_html"][0]["value"] == ""
434 or data["abstracts"][0]["value_html"] == ""
435 or len(data["contributors"]) == 0
436 or ("pdf" not in self.request.FILES and data["pdf"]["pdf_name"] == "No file uploaded")
437 ):
438 return JsonResponse({"status_code": 400})
440 data["colid"] = app_settings.COLID
442 article_data = self.convert_data_from_vuejs3(data)
443 title = article_data.titles[0]["title_html"]
444 abstract = article_data.abstracts[0]["value_html"]
446 self.submission.name = title
447 self.submission.abstract = abstract
448 self.submission._user = request.user
449 self.submission.save()
451 current_version = self.submission.current_version
452 current_version._user = request.user
453 current_version.save()
455 SubmissionAuthor.objects.filter(submission=self.submission).delete()
457 for contrib in article_data.contributors:
458 author = SubmissionAuthor(
459 submission=self.submission,
460 first_name=contrib["first_name"],
461 last_name=contrib["last_name"],
462 email=contrib["email"],
463 corresponding=contrib["corresponding"],
464 )
465 author.save()
467 data["redirect"] = {
468 "redirect": True,
469 "url": reverse_lazy("mesh:submission_info_update", kwargs={"pk": self.submission.pk}),
470 }
472 return JsonResponse(data)
475class SubmissionRedirectFromVue(TemplateView):
476 template_name = "mesh/forms/form_full_page_redirect.html"
478 def get_context_data(self, **kwargs):
479 context = super().get_context_data(**kwargs)
480 context["message"] = (
481 "La sauvegarde dans Vuejs puis la redirection sur une autre page fonctionne"
482 )
483 return context
485 def get(self, request, *args, **kwargs):
486 """
487 Override the get method to set the form action to "create".
488 """
490 messages.success(self.request, _("New submission successfully created."))
492 return super().get(request, *args, **kwargs)
495class SubmissionInfoView(RoleMixin, UpdateView):
496 """
497 View for updating the submission info.
498 """
500 model: type[Submission] = Submission
501 form_class = SubmissionInfoForm
502 template_name = "mesh/forms/submission_info.html"
503 submission: Submission
504 restricted_roles = [Author]
505 message_on_restrict = False
507 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
508 pk = kwargs["pk"]
509 self.submission = get_object_or_404(self.model, pk=pk)
511 return not self.role_handler.check_rights("can_edit_submission", self.submission)
513 def get_success_url(self) -> str:
514 # if FormAction.NEXT.value in self.request.POST:
515 # return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
517 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk})
519 def get_fail_redirect_uri(self) -> str:
520 """
521 Return to the submission details page if the user cannot update the submission.
522 """
523 return self.get_success_url()
525 def get_object(self, *args, **kwargs) -> Submission:
526 """
527 Override `get_object` built-in method to avoid an additional look-up when
528 we already have the object loaded.
529 """
530 return self.submission
532 def get_context_data(self, *args, **kwargs):
533 context = super().get_context_data(*args, **kwargs)
534 context["page_title"] = _("Edit submission info")
536 if self.submission.is_draft:
537 step_id = "info"
538 stepper = get_submission_stepper(self.submission)
539 stepper.set_active_step(step_id)
540 context["stepper"] = stepper
542 # No Save/Next button in the form, they are put in the stepper
543 context["form"].buttons = [] # = submission_stepper_form_buttons()
545 suggestions = self.submission.suggestions_for_reviewer.all()
546 for suggestion in suggestions:
547 reviewer = (
548 suggestion.suggested_reviewer
549 if suggestion.suggested_reviewer is not None
550 else suggestion.suggested_user
551 )
552 if suggestion.suggest_to_avoid:
553 context["avoid_reviewer"] = reviewer
554 else:
555 context["suggested_reviewer"] = reviewer
557 return context
559 def form_valid(self, form):
560 form.instance._user = self.request.user
562 Suggestion.objects.filter(submission=self.submission).delete()
564 for prefix in ["reviewer", "avoid"]:
565 person_info = {
566 "first_name": form.data.get(f"{prefix}_first_name", ""),
567 "last_name": form.data.get(f"{prefix}_last_name", ""),
568 "email": form.data.get(f"{prefix}_email", ""),
569 }
571 if person_info["email"] != "":
572 add_suggestion_from_person(
573 self.submission, person_info, suggest_to_avoid=prefix == "avoid"
574 )
576 return super().form_valid(form)
579class SubmissionEditArticleMetadataView(RoleMixin, UpdateView):
580 """
581 View for updating a submission (only the metadata).
582 Only the GET is used to display the form (with VueJS)
583 The POST is handled by SubmissionEditArticleMetadataVuejsAPIView
584 """
586 model: type[Submission] = Submission
587 form_class = SubmissionEditArticleMetadataForm
588 template_name = "mesh/forms/form_full_page.html"
589 submission: Submission
590 restricted_roles = [Author]
591 message_on_restrict = False
593 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
594 pk = kwargs["pk"]
595 self.submission = get_object_or_404(self.model, pk=pk)
597 return not self.role_handler.check_rights("can_edit_submission", self.submission)
599 # def get_fail_redirect_uri(self) -> str:
600 # """
601 # Return to the submission details page if the user cannot update the submission.
602 # Not used with VueJS
603 # """
604 # return self.get_success_url()
606 def get_object(self, *args, **kwargs) -> Submission:
607 """
608 Override `get_object` built-in method to avoid an additional look-up when
609 we already have the object loaded.
610 """
611 return self.submission
613 def get_context_data(self, *args, **kwargs):
614 context = super().get_context_data(*args, **kwargs)
615 context["page_title"] = _("Edit article metadata")
617 # data-get-url is used by VueJS to know the GET url to call
618 context["data_get_url"] = reverse_lazy(
619 "mesh:submission_update_vuejs",
620 kwargs={"pk": self.submission.pk},
621 )
623 if self.submission.is_draft:
624 step_id = "metadata"
625 stepper = get_submission_stepper(self.submission)
626 stepper.set_active_step(step_id)
627 context["stepper"] = stepper
629 # No Save/Next button in the form, they are put in the stepper
630 context["form"].buttons = [] # = submission_stepper_form_buttons()
632 # Generate breadcrumb data
633 # breadcrumb = get_submission_breadcrumb(self.submission)
634 # breadcrumb.add_item(
635 # title=_("Edit metadata"),
636 # url=reverse_lazy("mesh:submission_edit_article_metadata", kwargs={"pk": self.submission.pk}),
637 # )
638 # context["breadcrumb"] = breadcrumb
639 return context
641 def get_success_url(self) -> str:
642 if FormAction.NEXT.value in self.request.POST:
643 # No POST with VueJS. We shouldn't reach this clause
644 return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
645 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
647 # def form_valid(self, form: SubmissionUpdateForm) -> HttpResponse:
648 # form.instance._user = self.request.user
649 # if not form.changed_data:
650 # form.instance.do_not_update = True
651 #
652 # response = super().form_valid(form)
653 #
654 # if form.has_changed():
655 # SubmissionLog.add_message(
656 # self.submission,
657 # content=_("Modification of the submission's metadata"),
658 # content_en="Modification of the submission's metadata",
659 # user=self.request.user,
660 # )
661 # return response
664class SubmissionVersionCreateView(RoleMixin, SubmittableModelFormMixin, CreateView):
665 """
666 View for creating a new submission version.
667 """
669 model = SubmissionVersion
670 form_class = SubmissionVersionForm
671 template_name = "mesh/forms/form_full_page.html"
672 submission: Submission
673 restricted_roles = [Author]
674 save_submission = False
675 add_confirm_message = False
677 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
678 submission_pk = kwargs["submission_pk"]
679 self.submission = get_object_or_404(Submission, pk=submission_pk)
681 return not self.role_handler.check_rights("can_create_version", self.submission)
683 def get_success_url(self) -> str:
684 if FormAction.NEXT.value in self.request.POST:
685 # No POST with VueJS. We shouldn't reach this clause
686 return self.submit_url()
687 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
689 def submit_url(self) -> str:
690 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk})
692 # def form_pre_save(self, form: SubmissionVersionForm):
693 # Not used with VueJS
694 # form.instance._user = self.request.user
695 # form.instance.submission = self.submission
697 def get_context_data(self, *args, **kwargs):
698 context = super().get_context_data(*args, **kwargs)
699 current_version = self.submission.current_version
700 version = current_version.number + 1 if current_version else 1
701 context["page_title"] = _("Submission files")
702 if not self.submission.is_draft:
703 context["page_title"] += f" - Version {version}"
705 submission_url = reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
706 description = "Please fill the form below to submit your files for the submission "
707 description += f"<a href='{submission_url}'><i>{self.submission.name}</i></a>.<br>"
708 context["form_description"] = [description]
710 if self.submission.is_draft:
711 stepper = get_submission_stepper(self.submission)
712 stepper.set_active_step("version")
713 context["stepper"] = stepper
714 context["form"].buttons = []
716 # # Generate breadcrumb data
717 # breadcrumb = get_submission_breadcrumb(self.submission)
718 # breadcrumb.add_item(
719 # title=_("New version"),
720 # url=reverse_lazy(
721 # "mesh:submission_version_create", kwargs={"submission_pk": self.submission.pk}
722 # ),
723 # )
724 # context["breadcrumb"] = breadcrumb
725 return context
727 # def form_valid(self, form):
728 # result = super().form_valid(form)
729 # return result
732class SubmissionVersionUpdateView(RoleMixin, SubmittableModelFormMixin, UpdateView):
733 """
734 View for updating a submission version.
736 Submitting a version = submitting the whole submission.
737 This requires extra care about checking that all previous steps have been
738 taken when this is the first version.
739 """
741 model: type[SubmissionVersion] = SubmissionVersion
742 form_class = SubmissionVersionForm
743 template_name = "mesh/forms/form_full_page.html"
744 version: SubmissionVersion
745 restricted_roles = [Author]
746 message_on_restrict = False
747 save_submission = False
748 add_confirm_message = False
750 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
751 pk = kwargs["pk"]
752 self.version = get_object_or_404(self.model, pk=pk)
753 return not self.role_handler.check_rights("can_edit_version", self.version)
755 def get_fail_redirect_uri(self) -> str:
756 """
757 Redirects to the submission details page if the user cannot update the version.
758 """
759 return self.get_success_url()
761 def get_object(self, *args, **kwargs) -> SubmissionVersion:
762 """
763 Returns the already fetched version.
764 """
765 return self.version
767 def get_form_kwargs(self) -> dict[str, Any]:
768 kwargs = super().get_form_kwargs()
769 if self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true":
770 kwargs[SUBMIT_QUERY_PARAMETER] = True
771 return kwargs
773 def get_success_url(self) -> str:
774 if FormAction.NEXT.value in self.request.POST:
775 return self.submit_url()
776 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})
778 def submit_url(self) -> str:
779 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.version.submission.pk})
781 def form_pre_save(self, form: SubmissionVersionForm) -> None:
782 form.instance._user = self.request.user
784 def get_context_data(self, *args, **kwargs):
785 context = super().get_context_data(*args, **kwargs)
786 context["page_title"] = _(f"Submission files - Version {self.version.number}")
788 submission_url = reverse_lazy(
789 "mesh:submission_details", kwargs={"pk": self.version.submission.pk}
790 )
791 description = "Please fill the form below to submit your files for the submission "
792 description += f"<a href='{submission_url}'><i>{self.version.submission.name}</i></a>.<br>"
793 context["form_description"] = [description]
795 if self.version.submission.is_draft:
796 stepper = get_submission_stepper(self.version.submission)
797 stepper.set_active_step("version")
798 context["stepper"] = stepper
799 context["form"].buttons = []
801 # # Generate breadcrumb data
802 # breadcrumb = get_submission_breadcrumb(self.version.submission)
803 # breadcrumb.add_item(
804 # title=_("Edit version") + f" #{self.version.number}",
805 # url=reverse_lazy("mesh:submission_version_update", kwargs={"pk": self.version.pk}),
806 # )
807 # context["breadcrumb"] = breadcrumb
808 return context
810 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
811 """
812 View for updating the submission or deleting one of the associated files.
813 """
814 deletion_requested, _ = post_delete_model_file(self.version, request, self.role_handler)
816 if deletion_requested:
817 # Update the submission object.
818 self.version = get_object_or_404(self.model, pk=self.version.pk)
819 return self.get(request, *args, **kwargs)
821 return super().post(request, *args, **kwargs)
824class SubmissionResumeView(RoleMixin, View):
825 """
826 View handling the first submit of a submission. It resumes the submission process
827 at the correct edit view according to the missing data.
829 Handles the submission stepper. It redirects to the correct step according to
830 the submission status.
831 - STEP 1: Preprint id
832 - STEP 2: Article Metadata
833 - STEP 3: Submission Info
834 - STEP 4: Confirmation
835 """
837 submission: Submission
838 restricted_roles = [Author]
840 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
841 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
842 return not self.role_handler.check_rights("can_submit_submission", self.submission)
844 def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
845 stepper = get_submission_stepper(self.submission)
847 # Redirects to the last active step
848 for i in range(len(stepper.steps)):
849 step = stepper.steps[-(i + 1)]
850 if step.href and step.can_navigate:
851 return HttpResponseRedirect(step.href)
853 return HttpResponseRedirect(reverse_lazy("mesh:submission_create"))
856class SubmissionAuthorView(RoleMixin, TemplateView):
857 """
858 View to add/remove a submission author from a given submission.
859 """
861 submission: Submission
862 template_name = "mesh/forms/submission_author.html"
863 restricted_roles = [Author]
864 init_form: SubmissionAuthorForm | None = None
865 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding"
867 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
868 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
869 return not self.role_handler.check_rights("can_edit_submission", self.submission)
871 @cached_property
872 def authors(self) -> QuerySet[SubmissionAuthor]:
873 return SubmissionAuthor.objects.filter(submission=self.submission)
875 def get_context_data(self, *args, **kwargs):
876 context = super().get_context_data(*args, **kwargs)
878 context["submission"] = self.submission
880 authors = self.authors
881 context["authors"] = [
882 {
883 "author": author,
884 "delete_form": HiddenModelChoiceForm(
885 _queryset=authors,
886 form_action=FormAction.DELETE.value,
887 initial={"instance": author},
888 ),
889 "corresponding_form": HiddenModelChoiceForm(
890 _queryset=authors,
891 form_action="_action_toggle_corresponding",
892 initial={"instance": author},
893 ),
894 "buttons": [
895 Button(
896 id=f"author-corresponding-{author.pk}",
897 title=_("Corresponding"),
898 icon_class=("fa-toggle-on" if author.corresponding else "fa-toggle-off"),
899 form=HiddenModelChoiceForm(
900 _queryset=authors, initial={"instance": author}
901 ),
902 attrs={
903 "href": [
904 reverse_lazy(
905 "mesh:submission_authors",
906 kwargs={"pk": self.submission.pk},
907 )
908 ],
909 "type": ["submit"],
910 "class": ["primary" if author.corresponding else "inactive"],
911 "name": [self._FORM_ACTION_CORRESPONDING],
912 "data-tooltip": [
913 _("Click to toggle whether the author is a corresponding contact.")
914 ],
915 },
916 ),
917 Button(
918 id=f"author-delete-{author.pk}",
919 title=_("Remove"),
920 icon_class="fa-trash",
921 form=HiddenModelChoiceForm(
922 _queryset=authors, initial={"instance": author}
923 ),
924 attrs={
925 "href": [
926 reverse_lazy(
927 "mesh:submission_authors",
928 kwargs={"pk": self.submission.pk},
929 )
930 ],
931 "type": ["submit"],
932 "class": ["button-error"],
933 "name": [FormAction.DELETE.value],
934 },
935 ),
936 ],
937 }
938 for author in authors
939 ]
941 initial = {}
942 if not authors:
943 initial.update(
944 {
945 "first_name": self.request.user.first_name, # type:ignore
946 "last_name": self.request.user.last_name, # type:ignore
947 "email": self.request.user.email, # type:ignore
948 "primary": True,
949 }
950 )
952 form = self.init_form or SubmissionAuthorForm(
953 submission=self.submission, initial=initial or None
954 )
956 form.buttons = [
957 Button(
958 id="form_save",
959 title=_("Add author"),
960 icon_class="fa-plus",
961 attrs={"type": ["submit"], "class": ["save-button"]},
962 )
963 ]
965 context["page_title"] = _("Authors")
967 if self.submission.is_draft:
968 step_id = "authors"
969 stepper = get_submission_stepper(self.submission)
970 stepper.set_active_step(step_id)
971 context["stepper"] = stepper
972 # version_step = stepper.get_step("version")
973 # if version_step and version_step.can_navigate and version_step.href:
974 # button = Button(
975 # id="next",
976 # title=_("Next"),
977 # icon_class="fa-right-long",
978 # attrs={"href": [version_step.href], "class": ["as-button"]},
979 # )
980 # context["title_buttons"] = [button]
981 #
982 # form.buttons.append(
983 # Button(
984 # id="form_next",
985 # title=_("Next"),
986 # icon_class="fa-right-long",
987 # attrs={
988 # "href": [version_step.href],
989 # "type": ["submit"],
990 # "class": ["as-button", "save-button"],
991 # },
992 # )
993 # )
995 context["form"] = form
997 # # Generate breadcrumb data
998 # breadcrumb = get_submission_breadcrumb(self.submission)
999 # breadcrumb.add_item(
1000 # title=_("Authors"),
1001 # url=reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk}),
1002 # )
1003 # context["breadcrumb"] = breadcrumb
1005 return context
1007 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
1008 if FormAction.DELETE.value in request.POST:
1009 return self.remove_author(request)
1010 elif self._FORM_ACTION_CORRESPONDING in request.POST:
1011 return self.toggle_primary_author(request)
1013 return self.add_author(request, *args, **kwargs)
1015 def add_author(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
1016 """
1017 Add an `SubmissionAuthor` to the submission.
1018 """
1019 response = HttpResponseRedirect(
1020 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1021 )
1022 form = SubmissionAuthorForm(request.POST, submission=self.submission)
1023 if not form.is_valid():
1024 self.init_form = form
1025 return self.get(request, *args, **kwargs)
1027 form.instance._user = self.request.user
1028 form.instance.submission = self.submission
1029 form.save()
1031 SubmissionLog.add_message(
1032 self.submission,
1033 content=f"Author added: {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1034 content_en=_("Author added")
1035 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1036 user=request.user,
1037 significant=True,
1038 )
1039 messages.success(
1040 request,
1041 _("Author added")
1042 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})",
1043 )
1045 return response
1047 def remove_author(self, request: HttpRequest) -> HttpResponse:
1048 """
1049 Remove a `SubmissionAuthor` from the submission.
1050 """
1051 response = HttpResponseRedirect(
1052 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1053 )
1054 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
1055 if not form.is_valid():
1056 messages.error(request, _("Something went wrong. Please try again."))
1057 return response
1059 if len(self.authors) < 2:
1060 messages.error(
1061 request,
1062 _("There must be at least 1 author attached to the submission."),
1063 )
1064 return response
1066 author: SubmissionAuthor = form.cleaned_data["instance"]
1067 author_string = str(author)
1068 author.delete()
1070 SubmissionLog.add_message(
1071 self.submission,
1072 content=f"Author removed: {author_string}",
1073 content_en=_("Author added") + f": {author_string}",
1074 request=request.user,
1075 significant=True,
1076 )
1077 messages.success(request, _("The author has been removed."))
1078 return response
1080 def toggle_primary_author(self, request: HttpRequest) -> HttpResponse:
1081 """
1082 Toggle the `corresponding` boolean of a `SubmissionAuthor` from the submission.
1083 """
1084 response = HttpResponseRedirect(
1085 reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk})
1086 )
1087 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors)
1088 if not form.is_valid():
1089 messages.error(request, _("Something went wrong. Please try again."))
1090 return response
1092 author: SubmissionAuthor = form.cleaned_data["instance"]
1093 author.corresponding = not author.corresponding
1094 author.save()
1095 word = "marked" if author.corresponding else "unmarked"
1096 messages.success(
1097 request,
1098 _(f"Author {author} has been {word} as a corresponding contact."),
1099 )
1101 return response
1104class SubmissionConfirmView(RoleMixin, FormView):
1105 """
1106 View to confirm the submission the current SubmissionVersion.
1108 It's used both when submitting a Submission for the first time and when
1109 submitting a revised SubmissionVersion.
1110 """
1112 template_name = "mesh/submission/version_confirm.html"
1113 submission: Submission
1114 form_class = SubmissionConfirmForm
1116 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
1117 self.submission = get_object_or_404(Submission, pk=kwargs["pk"])
1119 if not self.role_handler.check_rights("can_edit_submission", self.submission):
1120 return True
1122 return not self.submission.is_submittable
1124 def get_success_url(self):
1125 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk})
1127 def get_object(self, queryset=None) -> SubmissionVersion:
1128 return self.submission.current_version # type:ignore
1130 def get_context_data(self, *args, **kwargs):
1131 context = super().get_context_data(*args, **kwargs)
1133 context["form"].buttons = [
1134 Button(
1135 id="form_save",
1136 title=_("Submit"),
1137 icon_class="fa-check",
1138 attrs={
1139 "type": ["submit"],
1140 "class": ["save-button", "button-highlight"],
1141 },
1142 )
1143 ]
1145 if self.submission.is_draft:
1146 stepper = get_submission_stepper(self.submission)
1147 stepper.set_active_step("confirm")
1148 context["stepper"] = stepper
1150 context["submission_proxy"] = SubmissionProxy(self.submission, self.role_handler)
1152 files = []
1153 files.append(
1154 {
1155 "file_wrapper": self.submission.current_version.main_file, # type:ignore
1156 "type": "Main",
1157 }
1158 )
1160 for file_wrapper in self.submission.current_version.additional_files.all(): # type:ignore
1161 files.append({"file_wrapper": file_wrapper, "type": "Additional"})
1162 context["submission_files"] = files
1164 # # Breadcrumb
1165 # breadcrumb = get_submission_breadcrumb(self.submission)
1166 # breadcrumb.add_item(
1167 # title=_("Authors"),
1168 # url=reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk}),
1169 # )
1170 # context["breadcrumb"] = breadcrumb
1172 return context
1174 def form_valid(self, form: SubmissionConfirmForm) -> HttpResponse:
1175 """
1176 Submit the submission's current version.
1177 """
1178 self.submission.submit(self.request.user)
1180 if self.submission.current_version.number == 1: # type:ignore
1181 messages.success(
1182 self.request,
1183 _("Your submission has been successfully saved and confirmed."),
1184 )
1185 else:
1186 messages.success(self.request, _("Your revisions were successfully submitted."))
1188 previous_round_number = self.submission.current_version.number - 1
1189 if previous_round_number > 0:
1190 previous_round = self.submission.versions.get(number=previous_round_number)
1191 previous_reviewers = [
1192 review.reviewer
1193 for review in previous_round.reviews.filter(state=ReviewState.SUBMITTED.value)
1194 ]
1195 for previous_reviewer in previous_reviewers:
1196 qs = Suggestion.objects.filter(
1197 submission=self.submission, suggested_user=previous_reviewer
1198 )
1199 if not qs.exists():
1200 add_suggestion(
1201 submission=self.submission,
1202 suggested_user=previous_reviewer,
1203 suggested_reviewer=None,
1204 )
1206 return HttpResponseRedirect(self.get_success_url())
1209class SubmissionDeleteView(RoleMixin, DeleteView):
1210 model: type[Submission] = Submission
1211 submission: Submission
1212 form_class = SubmissionDeleteForm
1213 template_name = "mesh/forms/submission_delete.html"
1214 restricted_roles = [JournalManager, Author]
1215 message_on_restrict = False
1217 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
1218 pk = kwargs["pk"]
1219 self.submission = get_object_or_404(self.model, pk=pk)
1221 return not self.role_handler.check_rights("can_edit_submission", self.submission)
1223 def get_object(self, *args, **kwargs) -> Submission:
1224 """
1225 Override `get_object` built-in method to avoid an additional look-up when
1226 we already have the object loaded.
1227 """
1228 return self.submission
1230 def get_success_url(self):
1231 return reverse_lazy("mesh:submission_list")
1233 def get_context_data(self, *args, **kwargs):
1234 context = super().get_context_data(*args, **kwargs)
1236 context["submission"] = self.submission
1237 context["form"].buttons = [
1238 Button(
1239 id="form_save",
1240 title=_("Delete"),
1241 icon_class="fa-trash",
1242 attrs={
1243 "type": ["submit"],
1244 "class": ["save-button", "button-highlight"],
1245 },
1246 )
1247 ]
1249 return context