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

1import json 

2from functools import cached_property 

3from typing import Any 

4 

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 

16 

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 

30 

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 

44 

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 

50 

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# ] 

66 

67 

68class SubmissionCreateView(RoleMixin, CreateView): 

69 """ 

70 View for creating a new submission with a preprint id. 

71 """ 

72 

73 model = Submission 

74 form_class = SubmissionCreateForm 

75 template_name = "mesh/forms/form_full_page.html" 

76 restricted_roles = [Author] 

77 

78 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

79 self.submission = None 

80 return not self.role_handler.check_rights("can_create_submission") 

81 

82 def get_success_url(self) -> str: 

83 """ 

84 Redirects to the SubmissionAuthor view to add authors to the 

85 newly created submission. 

86 """ 

87 

88 return reverse_lazy( 

89 "mesh:submission_edit_article_metadata", kwargs={"pk": self.submission.pk} 

90 ) 

91 

92 def form_valid(self, form: SubmissionCreateForm) -> HttpResponse: 

93 preprint_id = form.cleaned_data["preprint_id"] 

94 

95 title = "" 

96 abstract = "" 

97 article_data = None 

98 if preprint_id != "": 

99 article_data = fetch_article(preprint_id, with_bib=False) 

100 

101 title = article_data.title_html 

102 abstract = "" 

103 if len(article_data.abstracts) > 0: 

104 abstract = article_data.abstracts[0]["value_html"] 

105 

106 self.submission = Submission(name=title, abstract=abstract, author_agreement=False) 

107 self.submission._user = self.request.user 

108 self.submission.save() 

109 

110 version = SubmissionVersion(submission=self.submission) 

111 version._user = self.request.user 

112 version.save() 

113 

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() 

124 

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 ) 

132 

133 return HttpResponseRedirect(self.get_success_url()) 

134 

135 def get_context_data(self, *args, **kwargs): 

136 context = super().get_context_data(*args, **kwargs) 

137 context["page_title"] = _("New submission") 

138 

139 # context["data-get-url"] = reverse_lazy("mesh:submission_create_vuejs") 

140 

141 # Stepper 

142 step_id = "preprint" 

143 stepper = get_submission_stepper(None) 

144 stepper.set_active_step(step_id) 

145 context["stepper"] = stepper 

146 

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() 

150 

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 

157 

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 

163 

164 

165class SubmissionPreprintUpdateView(RoleMixin, UpdateView): 

166 """ 

167 View for updating a submission with a new preprint id. 

168 """ 

169 

170 model = Submission 

171 form_class = SubmissionCreateForm 

172 template_name = "mesh/forms/form_full_page.html" 

173 restricted_roles = [Author] 

174 

175 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

176 pk = kwargs["pk"] 

177 self.submission = get_object_or_404(self.model, pk=pk) 

178 

179 return not self.role_handler.check_rights("can_edit_submission", self.submission) 

180 

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 

187 

188 def get_success_url(self) -> str: 

189 """ 

190 Redirects to the SubmissionAuthor view to add authors to the 

191 newly created submission. 

192 """ 

193 

194 return reverse_lazy("mesh:submission_edit_article_metadata", kwargs={"pk": self.object.pk}) 

195 

196 def form_valid(self, form: SubmissionCreateForm) -> HttpResponse: 

197 preprint_id = form.cleaned_data["preprint_id"] 

198 

199 if preprint_id != "": 

200 article_data = fetch_article(preprint_id, with_bib=False) 

201 

202 title = article_data.title_html 

203 self.submission.name = title 

204 

205 if len(article_data.abstracts) > 0: 

206 abstract = article_data.abstracts[0]["value_tex"] 

207 self.submission.abstract = abstract 

208 

209 self.submission._user = self.request.user 

210 self.submission.save() 

211 

212 current_version = self.submission.current_version 

213 current_version._user = self.request.user 

214 current_version.save() 

215 

216 SubmissionAuthor.objects.filter(submission=self.submission).delete() 

217 

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() 

227 

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 ) 

235 

236 return HttpResponseRedirect(self.get_success_url()) 

237 

238 def get_context_data(self, *args, **kwargs): 

239 context = super().get_context_data(*args, **kwargs) 

240 context["page_title"] = _("Update submission preprint id") 

241 

242 # context["data-get-url"] = reverse_lazy("mesh:submission_create_vuejs") 

243 

244 # Stepper 

245 step_id = "preprint" 

246 stepper = get_submission_stepper(self.submission) 

247 stepper.set_active_step(step_id) 

248 context["stepper"] = stepper 

249 

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() 

253 

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 

260 

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 

266 

267 

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) 

351 

352 

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"] 

359 

360 self.submission = None 

361 self.version = None 

362 self.submission_main_file = None 

363 

364 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

365 pk = kwargs["pk"] 

366 self.submission = get_object_or_404(Submission, pk=pk) 

367 

368 return not self.role_handler.check_rights("can_edit_submission", self.submission) 

369 

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 

375 

376 def obj_to_dict(obj): 

377 return obj.__dict__ 

378 

379 data["article"]["colid"] = colid 

380 dump = json.dumps(data, default=obj_to_dict) 

381 return HttpResponse(dump, content_type="application/json") 

382 

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" 

390 

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() 

395 

396 submission_main_file = SubmissionMainFile( 

397 attached_to=self.submission.current_version, file=self.request.FILES["pdf"] 

398 ) 

399 submission_main_file.save() 

400 

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) 

408 

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) 

422 

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) 

428 

429 def post(self, request, *args, **kwargs): 

430 body_unicode = request.POST.get("data", "") 

431 data = json.loads(body_unicode) 

432 

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}) 

440 

441 data["colid"] = app_settings.COLID 

442 

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"] 

446 

447 self.submission.name = title 

448 self.submission.abstract = abstract 

449 self.submission._user = request.user 

450 self.submission.save() 

451 

452 current_version = self.submission.current_version 

453 current_version._user = request.user 

454 current_version.save() 

455 

456 SubmissionAuthor.objects.filter(submission=self.submission).delete() 

457 

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() 

467 

468 data["redirect"] = { 

469 "redirect": True, 

470 "url": reverse_lazy("mesh:submission_info_update", kwargs={"pk": self.submission.pk}), 

471 } 

472 

473 return JsonResponse(data) 

474 

475 

476class SubmissionRedirectFromVue(TemplateView): 

477 template_name = "mesh/forms/form_full_page_redirect.html" 

478 

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 

485 

486 def get(self, request, *args, **kwargs): 

487 """ 

488 Override the get method to set the form action to "create". 

489 """ 

490 

491 messages.success(self.request, _("New submission successfully created.")) 

492 

493 return super().get(request, *args, **kwargs) 

494 

495 

496class SubmissionInfoView(RoleMixin, UpdateView): 

497 """ 

498 View for updating the submission info. 

499 """ 

500 

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 

507 

508 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

509 pk = kwargs["pk"] 

510 self.submission = get_object_or_404(self.model, pk=pk) 

511 

512 return not self.role_handler.check_rights("can_edit_submission", self.submission) 

513 

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}) 

517 

518 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk}) 

519 

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() 

525 

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 

532 

533 def get_context_data(self, *args, **kwargs): 

534 context = super().get_context_data(*args, **kwargs) 

535 context["page_title"] = _("Edit submission info") 

536 

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 

542 

543 # No Save/Next button in the form, they are put in the stepper 

544 context["form"].buttons = [] # = submission_stepper_form_buttons() 

545 

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 

557 

558 return context 

559 

560 def form_valid(self, form): 

561 form.instance._user = self.request.user 

562 

563 Suggestion.objects.filter(submission=self.submission).delete() 

564 

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 } 

571 

572 if person_info["email"] != "": 

573 add_suggestion_from_person( 

574 self.submission, person_info, suggest_to_avoid=prefix == "avoid" 

575 ) 

576 

577 return super().form_valid(form) 

578 

579 

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 """ 

586 

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 

593 

594 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

595 pk = kwargs["pk"] 

596 self.submission = get_object_or_404(self.model, pk=pk) 

597 

598 return not self.role_handler.check_rights("can_edit_submission", self.submission) 

599 

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() 

606 

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 

613 

614 def get_context_data(self, *args, **kwargs): 

615 context = super().get_context_data(*args, **kwargs) 

616 context["page_title"] = _("Edit article metadata") 

617 

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 ) 

623 

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 

629 

630 # No Save/Next button in the form, they are put in the stepper 

631 context["form"].buttons = [] # = submission_stepper_form_buttons() 

632 

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 

641 

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}) 

647 

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 

663 

664 

665class SubmissionVersionCreateView(RoleMixin, SubmittableModelFormMixin, CreateView): 

666 """ 

667 View for creating a new submission version. 

668 """ 

669 

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 

677 

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) 

681 

682 return not self.role_handler.check_rights("can_create_version", self.submission) 

683 

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}) 

689 

690 def submit_url(self) -> str: 

691 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.submission.pk}) 

692 

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 

697 

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}" 

705 

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] 

710 

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 = [] 

716 

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 

727 

728 # def form_valid(self, form): 

729 # result = super().form_valid(form) 

730 # return result 

731 

732 

733class SubmissionVersionUpdateView(RoleMixin, SubmittableModelFormMixin, UpdateView): 

734 """ 

735 View for updating a submission version. 

736 

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 """ 

741 

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 

750 

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) 

755 

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() 

761 

762 def get_object(self, *args, **kwargs) -> SubmissionVersion: 

763 """ 

764 Returns the already fetched version. 

765 """ 

766 return self.version 

767 

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 

773 

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}) 

778 

779 def submit_url(self) -> str: 

780 return reverse_lazy("mesh:submission_confirm", kwargs={"pk": self.version.submission.pk}) 

781 

782 def form_pre_save(self, form: SubmissionVersionForm) -> None: 

783 form.instance._user = self.request.user 

784 

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}") 

788 

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] 

795 

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 = [] 

801 

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 

810 

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) 

816 

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) 

821 

822 return super().post(request, *args, **kwargs) 

823 

824 

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. 

829 

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 """ 

837 

838 submission: Submission 

839 restricted_roles = [Author] 

840 

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) 

844 

845 def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 

846 stepper = get_submission_stepper(self.submission) 

847 

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) 

853 

854 return HttpResponseRedirect(reverse_lazy("mesh:submission_create")) 

855 

856 

857class SubmissionAuthorView(RoleMixin, TemplateView): 

858 """ 

859 View to add/remove a submission author from a given submission. 

860 """ 

861 

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" 

867 

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) 

871 

872 @cached_property 

873 def authors(self) -> QuerySet[SubmissionAuthor]: 

874 return SubmissionAuthor.objects.filter(submission=self.submission) 

875 

876 def get_context_data(self, *args, **kwargs): 

877 context = super().get_context_data(*args, **kwargs) 

878 

879 context["submission"] = self.submission 

880 

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 ] 

941 

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 ) 

952 

953 form = self.init_form or SubmissionAuthorForm( 

954 submission=self.submission, initial=initial or None 

955 ) 

956 

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 ] 

965 

966 context["page_title"] = _("Authors") 

967 

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 # ) 

995 

996 context["form"] = form 

997 

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 

1005 

1006 return context 

1007 

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) 

1013 

1014 return self.add_author(request, *args, **kwargs) 

1015 

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) 

1027 

1028 form.instance._user = self.request.user 

1029 form.instance.submission = self.submission 

1030 form.save() 

1031 

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 ) 

1045 

1046 return response 

1047 

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 

1059 

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 

1066 

1067 author: SubmissionAuthor = form.cleaned_data["instance"] 

1068 author_string = str(author) 

1069 author.delete() 

1070 

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 

1080 

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 

1092 

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 ) 

1101 

1102 return response 

1103 

1104 

1105class SubmissionConfirmView(RoleMixin, FormView): 

1106 """ 

1107 View to confirm the submission the current SubmissionVersion. 

1108 

1109 It's used both when submitting a Submission for the first time and when 

1110 submitting a revised SubmissionVersion. 

1111 """ 

1112 

1113 template_name = "mesh/submission/version_confirm.html" 

1114 submission: Submission 

1115 form_class = SubmissionConfirmForm 

1116 

1117 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool: 

1118 self.submission = get_object_or_404(Submission, pk=kwargs["pk"]) 

1119 

1120 if not self.role_handler.check_rights("can_edit_submission", self.submission): 

1121 return True 

1122 

1123 return not self.submission.is_submittable 

1124 

1125 def get_success_url(self): 

1126 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.submission.pk}) 

1127 

1128 def get_object(self, queryset=None) -> SubmissionVersion: 

1129 return self.submission.current_version # type:ignore 

1130 

1131 def get_context_data(self, *args, **kwargs): 

1132 context = super().get_context_data(*args, **kwargs) 

1133 

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 ] 

1145 

1146 if self.submission.is_draft: 

1147 stepper = get_submission_stepper(self.submission) 

1148 stepper.set_active_step("confirm") 

1149 context["stepper"] = stepper 

1150 

1151 context["submission_proxy"] = SubmissionProxy(self.submission, self.role_handler) 

1152 

1153 files = [] 

1154 files.append( 

1155 { 

1156 "file_wrapper": self.submission.current_version.main_file, # type:ignore 

1157 "type": "Main", 

1158 } 

1159 ) 

1160 

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 

1164 

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 

1172 

1173 return context 

1174 

1175 def form_valid(self, form: SubmissionConfirmForm) -> HttpResponse: 

1176 """ 

1177 Submit the submission's current version. 

1178 """ 

1179 self.submission.submit(self.request.user) 

1180 

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.")) 

1188 

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 ) 

1206 

1207 return HttpResponseRedirect(self.get_success_url()) 

1208 

1209 

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 

1217 

1218 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs): 

1219 pk = kwargs["pk"] 

1220 self.submission = get_object_or_404(self.model, pk=pk) 

1221 

1222 return not self.role_handler.check_rights("can_edit_submission", self.submission) 

1223 

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 

1230 

1231 def get_success_url(self): 

1232 return reverse_lazy("mesh:submission_list") 

1233 

1234 def get_context_data(self, *args, **kwargs): 

1235 context = super().get_context_data(*args, **kwargs) 

1236 

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 ] 

1249 

1250 return context