Coverage for src / mesh / views / views_submission_edit.py: 48%

503 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-05-04 12:41 +0000

1import json 

2from typing import Any 

3 

4from django.contrib import messages 

5from django.contrib.auth.mixins import LoginRequiredMixin 

6from django.http import HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse 

7from django.urls.base import reverse 

8from django.utils.translation import gettext_lazy as _ 

9from django.views.generic import FormView, TemplateView, View 

10from django.views.generic.edit import CreateView, DeleteView, UpdateView 

11from matching_back.views import ArticleEditFormWithVueAPIView 

12from ptf.external.fetch_metadata import fetch_article 

13from ptf.model_data import create_articledata, create_contributor 

14 

15from mesh.app_settings import app_settings 

16from mesh.models.crud import create_submission 

17from mesh.models.file_helpers import post_delete_model_file 

18from mesh.models.orm.review_models import ReviewState 

19from mesh.models.orm.submission_models import ( 

20 Submission, 

21 SubmissionAuthor, 

22 SubmissionLog, 

23 SubmissionMainFile, 

24 SubmissionVersion, 

25) 

26from mesh.models.orm.suggestion_model import Suggestion 

27from mesh.models.roles.author import Author 

28from mesh.models.roles.journal_manager import JournalManager 

29from mesh.views.components.button import Button 

30from mesh.views.components.stepper import get_submission_stepper 

31from mesh.views.forms.base_forms import ( 

32 SUBMIT_QUERY_PARAMETER, 

33 FormAction, 

34 HiddenModelChoiceForm, 

35) 

36from mesh.views.forms.submission_forms import ( 

37 SubmissionAuthorForm, 

38 SubmissionConfirmForm, 

39 SubmissionCreateForm, 

40 SubmissionDeleteForm, 

41 SubmissionEditArticleMetadataForm, 

42 SubmissionInfoForm, 

43 SubmissionVersionForm, 

44) 

45from mesh.views.viewmodel import SubmissionProxy 

46from mesh.views.views_base import ( 

47 MeshObjectMixin, 

48 SubmittableModelFormMixin, 

49) 

50from mesh.views.views_reviewer import add_suggestion, add_suggestion_from_person 

51 

52 

53class SubmissionCreateView(LoginRequiredMixin, CreateView): 

54 """ 

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

56 """ 

57 

58 model = Submission 

59 form_class = SubmissionCreateForm 

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

61 restricted_roles = [Author] 

62 

63 def setup(self, request, *args, **kwargs): 

64 super().setup(request, *args, **kwargs) 

65 if not self.request.current_role.can_create_submission(): 

66 raise PermissionError 

67 self.submission = None 

68 

69 def get_success_url(self) -> str: 

70 """ 

71 Redirects to the SubmissionAuthor view to add authors to the 

72 newly created submission. 

73 """ 

74 

75 return reverse( 

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

77 ) 

78 

79 def form_valid(self, form) -> HttpResponse: 

80 # raise NotImplementedError("TODO : FIX FORM VALIDATION : split logic") 

81 self.submission = create_submission( 

82 user=self.request.user, preprint_id=form.cleaned_data["preprint_id"] 

83 ) 

84 return HttpResponseRedirect(self.get_success_url()) 

85 

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

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

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

89 

90 # context["data-get-url"] = reverse("mesh:submission_create_vuejs") 

91 

92 # Stepper 

93 step_id = "preprint" 

94 stepper = get_submission_stepper(None) 

95 stepper.set_active_step(step_id) 

96 context["stepper"] = stepper 

97 

98 # Form buttons 

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

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

101 

102 descriptions = [ 

103 "You are about to start the submit process.", 

104 'Please read our guidelines <a href="">here</a>', 

105 "If you have a preprint, you can enter its id below. Leave the field blank otherwise.", 

106 ] 

107 context["form_description"] = descriptions 

108 

109 # # Generate breadcrumb data 

110 # breadcrumb = get_base_breadcrumb() 

111 # breadcrumb.add_item(title=_("New submission"), url=reverse("mesh:submission_create")) 

112 # context["breadcrumb"] = breadcrumb 

113 return context 

114 

115 

116class SubmissionPreprintUpdateView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

117 """ 

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

119 """ 

120 

121 model = Submission 

122 form_class = SubmissionCreateForm 

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

124 restricted_roles = [Author] 

125 

126 def setup(self, request, *args, **kwargs): 

127 super().setup(request, *args, **kwargs) 

128 self.submission = self.get_submission() 

129 if not self.request.current_role.can_edit_submission(self.submission): 

130 raise PermissionError 

131 

132 def get_object(self, *args, **kwargs) -> Submission: 

133 """ 

134 Override `get_object` built-in method to avoid an additional look-up when 

135 we already have the object loaded. 

136 """ 

137 return self.submission 

138 

139 def get_success_url(self) -> str: 

140 """ 

141 Redirects to the SubmissionAuthor view to add authors to the 

142 newly created submission. 

143 """ 

144 

145 return reverse( 

146 "mesh:submission_edit_article_metadata", kwargs={"submission_pk": self.object.pk} 

147 ) 

148 

149 def form_valid(self, form): 

150 preprint_id = form.cleaned_data["preprint_id"] 

151 

152 if preprint_id != "": 

153 article_data = fetch_article(preprint_id, with_bib=False) 

154 

155 title = article_data.title_html 

156 self.submission.name = title 

157 

158 if len(article_data.abstracts) > 0: 

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

160 self.submission.abstract = abstract 

161 

162 self.submission._user = self.request.user 

163 self.submission.save() 

164 

165 current_version = self.submission.get_current_version() 

166 current_version._user = self.request.user 

167 current_version.save() 

168 

169 self.submission.authors.all().delete() 

170 

171 for contrib in article_data.contributors: 

172 author = SubmissionAuthor( 

173 submission=self.submission, 

174 first_name=contrib["first_name"], 

175 last_name=contrib["last_name"], 

176 email=contrib["email"], 

177 corresponding=contrib["corresponding"], 

178 ) 

179 author.save() 

180 

181 SubmissionLog.add_message( 

182 self.submission, 

183 content=_("Update of the submission with a preprint id"), 

184 content_en="Update of the submission with a preprint id", 

185 user=self.request.user, 

186 significant=True, 

187 ) 

188 

189 return HttpResponseRedirect(self.get_success_url()) 

190 

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

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

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

194 

195 # context["data-get-url"] = reverse("mesh:submission_create_vuejs") 

196 

197 # Stepper 

198 step_id = "preprint" 

199 stepper = get_submission_stepper(self.submission) 

200 stepper.set_active_step(step_id) 

201 context["stepper"] = stepper 

202 

203 # Form buttons 

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

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

206 

207 descriptions = [ 

208 "You are about to start the submit process.", 

209 'Please read our guidelines <a href="">here</a>', 

210 "If you have a preprint, you can enter its id below. Leave the field blank otherwise.", 

211 ] 

212 context["form_description"] = descriptions 

213 

214 # # Generate breadcrumb data 

215 # breadcrumb = get_base_breadcrumb() 

216 # breadcrumb.add_item(title=_("New submission"), url=reverse("mesh:submission_create")) 

217 # context["breadcrumb"] = breadcrumb 

218 return context 

219 

220 

221class SubmissionEditArticleMetadataVuejsAPIView( 

222 LoginRequiredMixin, MeshObjectMixin, ArticleEditFormWithVueAPIView 

223): 

224 def __init__(self, **kwargs): 

225 super().__init__(**kwargs) 

226 self.fields_to_update = ["contributors", "abstracts", "titles", "pdf"] 

227 self.editorial_tools = ["light_authors", "inline_ckeditor"] 

228 self.mandatory_fields = ["contributors", "abstract", "title", "pdf"] 

229 

230 self.submission = None 

231 self.version = None 

232 self.submission_main_file = None 

233 

234 def setup(self, request, *args, **kwargs): 

235 super().setup(request, *args, **kwargs) 

236 self.submission = self.get_submission() 

237 if not self.request.current_role.can_edit_submission(self.submission): 

238 raise PermissionError 

239 

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

241 colid = app_settings.COLID 

242 data_article = create_articledata() 

243 data = self.convert_data_for_vuejs3(data_article, colid=colid) 

244 data["editorial_tools"] = self.editorial_tools 

245 

246 def obj_to_dict(obj): 

247 return obj.__dict__ 

248 

249 data["article"]["colid"] = colid 

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

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

252 

253 def handle_pdf_for_vuejs3(self, data, data_article, method=""): 

254 if method == "get": 

255 submission_main_file = getattr( 

256 self.submission.get_current_version(), "main_file", None 

257 ) 

258 if submission_main_file is not None: 

259 data["article"]["pdf_name"] = submission_main_file.file.name 

260 else: 

261 data["article"]["pdf_name"] = "No file uploaded" 

262 

263 elif method == "post" and "pdf" in self.request.FILES: 

264 submission_main_file = getattr( 

265 self.submission.get_current_version(), "main_file", None 

266 ) 

267 if submission_main_file is not None: 

268 submission_main_file.delete() 

269 

270 submission_main_file = SubmissionMainFile( 

271 attached_to=self.submission.get_current_version(), file=self.request.FILES["pdf"] 

272 ) 

273 submission_main_file.save() 

274 

275 def handle_titles_for_vuejs3(self, data, data_article, method=""): 

276 if method == "get": 

277 data["article"]["titles"] = [ 

278 {"lang": data_article.lang, "value": self.submission.name} 

279 ] 

280 else: 

281 super().handle_titles_for_vuejs3(data, data_article, method) 

282 

283 def handle_contributors_for_vuejs3(self, data, data_article, method=""): 

284 if method == "get": 

285 contributors = [] 

286 for author in self.submission.authors.all(): 

287 contrib = create_contributor() 

288 contrib["first_name"] = author.first_name 

289 contrib["last_name"] = author.last_name 

290 contrib["email"] = author.email 

291 contrib["corresponding"] = author.corresponding 

292 contributors.append(contrib) 

293 data["article"]["contributors"] = contributors 

294 else: 

295 super().handle_contributors_for_vuejs3(data, data_article, method) 

296 

297 def handle_abstracts_for_vuejs3(self, data, data_article, method=""): 

298 if method == "get": 

299 data["article"]["abstracts"] = [{"lang": "en", "value": self.submission.abstract}] 

300 else: 

301 super().handle_abstracts_for_vuejs3(data, data_article, method) 

302 

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

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

305 data = json.loads(body_unicode) 

306 

307 if ( 

308 data["title_html"][0]["value"] == "" 

309 or data["abstracts"][0]["value"] == "" 

310 or len(data["contributors"]) == 0 

311 or ("pdf" not in self.request.FILES and data["pdf"]["pdf_name"] == "No file uploaded") 

312 ): 

313 return JsonResponse({"status_code": 400}) 

314 

315 data["colid"] = app_settings.COLID 

316 

317 article_data = self.convert_data_from_vuejs3(data) 

318 title = article_data.title_html 

319 

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

321 

322 self.submission.name = title 

323 self.submission.abstract = abstract 

324 self.submission._user = request.user 

325 self.submission.save() 

326 

327 current_version = self.submission.get_current_version() 

328 current_version._user = request.user 

329 current_version.save() 

330 

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

332 

333 for contrib in article_data.contributors: 

334 author = SubmissionAuthor( 

335 submission=self.submission, 

336 first_name=contrib["first_name"], 

337 last_name=contrib["last_name"], 

338 email=contrib["email"], 

339 corresponding=contrib["corresponding"], 

340 ) 

341 author.save() 

342 

343 data["redirect"] = { 

344 "redirect": True, 

345 "url": reverse( 

346 "mesh:submission_info_update", kwargs={"submission_pk": self.submission.pk} 

347 ), 

348 } 

349 

350 return JsonResponse(data) 

351 

352 

353class SubmissionRedirectFromVue(TemplateView): 

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

355 

356 def get_context_data(self, **kwargs): 

357 context = super().get_context_data(**kwargs) 

358 context["message"] = ( 

359 "La sauvegarde dans Vuejs puis la redirection sur une autre page fonctionne" 

360 ) 

361 return context 

362 

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

364 """ 

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

366 """ 

367 

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

369 

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

371 

372 

373class SubmissionInfoView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

374 """ 

375 View for updating the submission info. 

376 """ 

377 

378 model = Submission 

379 form_class = SubmissionInfoForm 

380 template_name = "mesh/forms/submission_info.html" 

381 submission: Submission 

382 restricted_roles = [Author] 

383 message_on_restrict = False 

384 

385 def setup(self, request, *args, **kwargs): 

386 super().setup(request, *args, **kwargs) 

387 self.submission = self.get_submission() 

388 if not self.request.current_role.can_edit_submission(self.submission): 

389 raise PermissionError 

390 

391 def get_success_url(self) -> str: 

392 # if FormAction.NEXT.value in self.request.POST: 

393 # return reverse_lazy("mesh:submission_authors", kwargs={"pk": self.submission.pk}) 

394 

395 return reverse("mesh:submission_confirm", kwargs={"submission_pk": self.submission.pk}) 

396 

397 def get_fail_redirect_uri(self) -> str: 

398 """ 

399 Return to the submission details page if the user cannot update the submission. 

400 """ 

401 return self.get_success_url() 

402 

403 def get_object(self, *args, **kwargs) -> Submission: 

404 """ 

405 Override `get_object` built-in method to avoid an additional look-up when 

406 we already have the object loaded. 

407 """ 

408 return self.submission 

409 

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

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

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

413 

414 if self.submission.is_draft: 

415 step_id = "info" 

416 stepper = get_submission_stepper(self.submission) 

417 stepper.set_active_step(step_id) 

418 context["stepper"] = stepper 

419 

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

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

422 

423 suggestions = self.submission.suggestions_for_reviewer.all() 

424 for suggestion in suggestions: 

425 reviewer = ( 

426 suggestion.suggested_reviewer 

427 if suggestion.suggested_reviewer is not None 

428 else suggestion.suggested_user 

429 ) 

430 if suggestion.suggest_to_avoid: 

431 context["avoid_reviewer"] = reviewer 

432 else: 

433 context["suggested_reviewer"] = reviewer 

434 

435 return context 

436 

437 def form_valid(self, form): 

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

439 

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

441 

442 for prefix in ["reviewer", "avoid"]: 

443 person_info = { 

444 "first_name": form.data.get(f"{prefix}_first_name", ""), 

445 "last_name": form.data.get(f"{prefix}_last_name", ""), 

446 "email": form.data.get(f"{prefix}_email", ""), 

447 } 

448 

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

450 add_suggestion_from_person( 

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

452 ) 

453 

454 return super().form_valid(form) 

455 

456 

457class SubmissionEditArticleMetadataView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

458 """ 

459 View for updating a submission (only the metadata). 

460 Only the GET is used to display the form (with VueJS) 

461 The POST is handled by SubmissionEditArticleMetadataVuejsAPIView 

462 """ 

463 

464 model = Submission 

465 form_class = SubmissionEditArticleMetadataForm 

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

467 submission: Submission 

468 restricted_roles = [Author] 

469 message_on_restrict = False 

470 

471 def setup(self, request, *args, **kwargs): 

472 super().setup(request, *args, **kwargs) 

473 self.submission = self.get_submission() 

474 if not self.request.current_role.can_edit_submission(self.submission): 

475 raise PermissionError 

476 

477 # def get_fail_redirect_uri(self) -> str: 

478 # """ 

479 # Return to the submission details page if the user cannot update the submission. 

480 # Not used with VueJS 

481 # """ 

482 # return self.get_success_url() 

483 

484 def get_object(self, *args, **kwargs) -> Submission: 

485 """ 

486 Override `get_object` built-in method to avoid an additional look-up when 

487 we already have the object loaded. 

488 """ 

489 return self.submission 

490 

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

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

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

494 

495 # data-get-url is used by VueJS to know the GET url to call 

496 context["data_get_url"] = reverse( 

497 "mesh:submission_update_vuejs", 

498 kwargs={"submission_pk": self.submission.pk}, 

499 ) 

500 

501 if self.submission.is_draft: 

502 step_id = "metadata" 

503 stepper = get_submission_stepper(self.submission) 

504 stepper.set_active_step(step_id) 

505 context["stepper"] = stepper 

506 

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

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

509 

510 # Generate breadcrumb data 

511 # breadcrumb = get_submission_breadcrumb(self.submission) 

512 # breadcrumb.add_item( 

513 # title=_("Edit metadata"), 

514 # url=reverse_lazy("mesh:submission_edit_article_metadata", kwargs={"pk": self.submission.pk}), 

515 # ) 

516 # context["breadcrumb"] = breadcrumb 

517 return context 

518 

519 def get_success_url(self) -> str: 

520 if FormAction.NEXT.value in self.request.POST: 

521 # No POST with VueJS. We shouldn't reach this clause 

522 return reverse("mesh:submission_authors", kwargs={"submission_pk": self.submission.pk}) 

523 return reverse("mesh:submission_details", kwargs={"submission_pk": self.submission.pk}) 

524 

525 # def form_valid(self, form: SubmissionUpdateForm) -> HttpResponse: 

526 # form.instance._user = self.request.user 

527 # if not form.changed_data: 

528 # form.instance.do_not_update = True 

529 # 

530 # response = super().form_valid(form) 

531 # 

532 # if form.has_changed(): 

533 # SubmissionLog.add_message( 

534 # self.submission, 

535 # content=_("Modification of the submission's metadata"), 

536 # content_en="Modification of the submission's metadata", 

537 # user=self.request.user, 

538 # ) 

539 # return response 

540 

541 

542class SubmissionVersionCreateView( 

543 LoginRequiredMixin, MeshObjectMixin, SubmittableModelFormMixin, CreateView 

544): 

545 """ 

546 View for creating a new submission version. 

547 """ 

548 

549 model = SubmissionVersion 

550 form_class = SubmissionVersionForm 

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

552 submission: Submission 

553 restricted_roles = [Author] 

554 save_submission = False 

555 add_confirm_message = False 

556 

557 def setup(self, request, *args, **kwargs): 

558 super().setup(request, *args, **kwargs) 

559 self.submission = self.get_submission() 

560 if not self.request.current_role.can_create_version(self.submission): 

561 raise PermissionError 

562 

563 def get_form_kwargs(self): 

564 kwargs = super().get_form_kwargs() 

565 kwargs["submission"] = self.submission 

566 return kwargs 

567 

568 def form_valid(self, form): 

569 form.instance.submission = self.submission 

570 return super().form_valid(form) 

571 

572 def get_success_url(self) -> str: 

573 if FormAction.NEXT.value in self.request.POST: 

574 # No POST with VueJS. We shouldn't reach this clause 

575 return self.submit_url() 

576 return reverse("mesh:submission_details", kwargs={"submission_pk": self.submission.pk}) 

577 

578 def submit_url(self) -> str: 

579 return reverse("mesh:submission_confirm", kwargs={"submission_pk": self.submission.pk}) 

580 

581 # def form_pre_save(self, form: SubmissionVersionForm): 

582 # Not used with VueJS 

583 # form.instance._user = self.request.user 

584 # form.instance.submission = self.submission 

585 

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

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

588 current_version = self.submission.get_current_version() 

589 version = current_version.number + 1 if current_version else 1 

590 context["page_title"] = _("Submission files") 

591 if not self.submission.is_draft: 

592 context["page_title"] += f" - Version {version}" 

593 

594 submission_url = reverse( 

595 "mesh:submission_details", kwargs={"submission_pk": self.submission.pk} 

596 ) 

597 description = "Please fill the form below to submit your files for the submission " 

598 description += f"<a href='{submission_url}'><i>{self.submission.name}</i></a>.<br>" 

599 context["form_description"] = [description] 

600 

601 if self.submission.is_draft: 

602 stepper = get_submission_stepper(self.submission) 

603 stepper.set_active_step("version") 

604 context["stepper"] = stepper 

605 context["form"].buttons = [] 

606 

607 # # Generate breadcrumb data 

608 # breadcrumb = get_submission_breadcrumb(self.submission) 

609 # breadcrumb.add_item( 

610 # title=_("New version"), 

611 # url=reverse_lazy( 

612 # "mesh:submission_version_create", kwargs={"submission_pk": self.submission.pk} 

613 # ), 

614 # ) 

615 # context["breadcrumb"] = breadcrumb 

616 return context 

617 

618 # def form_valid(self, form): 

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

620 # return result 

621 

622 

623class SubmissionVersionUpdateView( 

624 LoginRequiredMixin, MeshObjectMixin, SubmittableModelFormMixin, UpdateView 

625): 

626 """ 

627 View for updating a submission version. 

628 

629 Submitting a version = submitting the whole submission. 

630 This requires extra care about checking that all previous steps have been 

631 taken when this is the first version. 

632 """ 

633 

634 model = SubmissionVersion 

635 form_class = SubmissionVersionForm 

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

637 

638 restricted_roles = [Author] 

639 message_on_restrict = False 

640 save_submission = False 

641 add_confirm_message = False 

642 

643 def setup(self, request, *args, **kwargs): 

644 super().setup(request, *args, **kwargs) 

645 self.version = self.get_version() 

646 if not self.request.current_role.can_edit_version(self.submission): 

647 raise PermissionError 

648 

649 def get_fail_redirect_uri(self) -> str: 

650 """ 

651 Redirects to the submission details page if the user cannot update the version. 

652 """ 

653 return self.get_success_url() 

654 

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

656 """ 

657 Returns the already fetched version. 

658 """ 

659 return self.version 

660 

661 def get_form_kwargs(self) -> dict[str, Any]: 

662 kwargs = super().get_form_kwargs() 

663 if self.request.GET.get(SUBMIT_QUERY_PARAMETER, None) == "true": 

664 kwargs[SUBMIT_QUERY_PARAMETER] = True 

665 return kwargs 

666 

667 def get_success_url(self) -> str: 

668 if FormAction.NEXT.value in self.request.POST: 

669 return self.submit_url() 

670 return reverse( 

671 "mesh:submission_details", kwargs={"submission_pk": self.version.submission.pk} 

672 ) 

673 

674 def submit_url(self) -> str: 

675 return reverse( 

676 "mesh:submission_confirm", kwargs={"submission_pk": self.version.submission.pk} 

677 ) 

678 

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

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

681 

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

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

684 context["page_title"] = _(f"Submission files - Version {self.version.number}") 

685 

686 submission_url = reverse( 

687 "mesh:submission_details", kwargs={"submission_pk": self.version.submission.pk} 

688 ) 

689 description = "Please fill the form below to submit your files for the submission " 

690 description += f"<a href='{submission_url}'><i>{self.version.submission.name}</i></a>.<br>" 

691 context["form_description"] = [description] 

692 

693 if self.version.submission.is_draft: 

694 stepper = get_submission_stepper(self.version.submission) 

695 stepper.set_active_step("version") 

696 context["stepper"] = stepper 

697 context["form"].buttons = [] 

698 

699 # # Generate breadcrumb data 

700 # breadcrumb = get_submission_breadcrumb(self.version.submission) 

701 # breadcrumb.add_item( 

702 # title=_("Edit version") + f" #{self.version.number}", 

703 # url=reverse("mesh:submission_version_update", kwargs={"pk": self.version.pk}), 

704 # ) 

705 # context["breadcrumb"] = breadcrumb 

706 return context 

707 

708 def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 

709 """ 

710 View for updating the submission or deleting one of the associated files. 

711 """ 

712 deletion_requested, _ = post_delete_model_file(self.version, request, self.role_handler) 

713 

714 if deletion_requested: 

715 # Update the submission object. 

716 self.version = self.get_version() 

717 return self.get(request, *args, **kwargs) 

718 

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

720 

721 

722class SubmissionResumeView(LoginRequiredMixin, MeshObjectMixin, View): 

723 """ 

724 View handling the first submit of a submission. It resumes the submission process 

725 at the correct edit view according to the missing data. 

726 

727 Handles the submission stepper. It redirects to the correct step according to 

728 the submission status. 

729 - STEP 1: Preprint id 

730 - STEP 2: Article Metadata 

731 - STEP 3: Submission Info 

732 - STEP 4: Confirmation 

733 """ 

734 

735 submission: Submission 

736 restricted_roles = [Author] 

737 

738 def setup(self, request, *args, **kwargs): 

739 super().setup(request, *args, **kwargs) 

740 self.submission = self.get_submission() 

741 if not self.request.current_role.can_submit_submission(self.submission): 

742 raise PermissionError 

743 

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

745 stepper = get_submission_stepper(self.submission) 

746 

747 # Redirects to the last active step 

748 for i in range(len(stepper.steps)): 

749 step = stepper.steps[-(i + 1)] 

750 if step.href and step.can_navigate: 

751 return HttpResponseRedirect(step.href) 

752 

753 return HttpResponseRedirect(reverse("mesh:submission_create")) 

754 

755 

756class SubmissionAuthorView(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

757 """ 

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

759 """ 

760 

761 submission: Submission 

762 template_name = "mesh/forms/submission_author.html" 

763 restricted_roles = [Author] 

764 init_form: SubmissionAuthorForm | None = None 

765 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding" 

766 

767 def setup(self, request, *args, **kwargs): 

768 super().setup(request, *args, **kwargs) 

769 self.submission = self.get_submission() 

770 if not self.request.current_role.can_edit_submission(self.submission): 

771 raise PermissionError 

772 

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

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

775 

776 context["submission"] = self.submission 

777 

778 authors = self.submission.authors_censored 

779 context["authors"] = [ 

780 { 

781 "author": author, 

782 "delete_form": HiddenModelChoiceForm( 

783 _queryset=authors, 

784 form_action=FormAction.DELETE.value, 

785 initial={"instance": author}, 

786 ), 

787 "corresponding_form": HiddenModelChoiceForm( 

788 _queryset=authors, 

789 form_action="_action_toggle_corresponding", 

790 initial={"instance": author}, 

791 ), 

792 "buttons": [ 

793 Button( 

794 id=f"author-corresponding-{author.pk}", 

795 title=_("Corresponding"), 

796 icon_class=("fa-toggle-on" if author.corresponding else "fa-toggle-off"), 

797 form=HiddenModelChoiceForm( 

798 _queryset=authors, initial={"instance": author} 

799 ), 

800 attrs={ 

801 "href": [ 

802 reverse( 

803 "mesh:submission_authors", 

804 kwargs={"submission_pk": self.submission.pk}, 

805 ) 

806 ], 

807 "type": ["submit"], 

808 "class": ["primary" if author.corresponding else "inactive"], 

809 "name": [self._FORM_ACTION_CORRESPONDING], 

810 "data-tooltip": [ 

811 _("Click to toggle whether the author is a corresponding contact.") 

812 ], 

813 }, 

814 ), 

815 Button( 

816 id=f"author-delete-{author.pk}", 

817 title=_("Remove"), 

818 icon_class="fa-trash", 

819 form=HiddenModelChoiceForm( 

820 _queryset=authors, initial={"instance": author} 

821 ), 

822 attrs={ 

823 "href": [ 

824 reverse( 

825 "mesh:submission_authors", 

826 kwargs={"submission_pk": self.submission.pk}, 

827 ) 

828 ], 

829 "type": ["submit"], 

830 "class": ["button-error"], 

831 "name": [FormAction.DELETE.value], 

832 }, 

833 ), 

834 ], 

835 } 

836 for author in authors 

837 ] 

838 

839 initial = {} 

840 if not authors: 

841 initial.update( 

842 { 

843 "first_name": self.request.user.first_name, 

844 "last_name": self.request.user.last_name, 

845 "email": self.request.user.email, 

846 "primary": True, 

847 } 

848 ) 

849 

850 form = self.init_form or SubmissionAuthorForm( 

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

852 ) 

853 

854 form.buttons = [ 

855 Button( 

856 id="form_save", 

857 title=_("Add author"), 

858 icon_class="fa-plus", 

859 attrs={"type": ["submit"], "class": ["save-button"]}, 

860 ) 

861 ] 

862 

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

864 

865 if self.submission.is_draft: 

866 step_id = "authors" 

867 stepper = get_submission_stepper(self.submission) 

868 stepper.set_active_step(step_id) 

869 context["stepper"] = stepper 

870 # version_step = stepper.get_step("version") 

871 # if version_step and version_step.can_navigate and version_step.href: 

872 # button = Button( 

873 # id="next", 

874 # title=_("Next"), 

875 # icon_class="fa-right-long", 

876 # attrs={"href": [version_step.href], "class": ["as-button"]}, 

877 # ) 

878 # context["title_buttons"] = [button] 

879 # 

880 # form.buttons.append( 

881 # Button( 

882 # id="form_next", 

883 # title=_("Next"), 

884 # icon_class="fa-right-long", 

885 # attrs={ 

886 # "href": [version_step.href], 

887 # "type": ["submit"], 

888 # "class": ["as-button", "save-button"], 

889 # }, 

890 # ) 

891 # ) 

892 

893 context["form"] = form 

894 

895 # # Generate breadcrumb data 

896 # breadcrumb = get_submission_breadcrumb(self.submission) 

897 # breadcrumb.add_item( 

898 # title=_("Authors"), 

899 # url=reverse("mesh:submission_authors", kwargs={"pk": self.submission.pk}), 

900 # ) 

901 # context["breadcrumb"] = breadcrumb 

902 

903 return context 

904 

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

906 if FormAction.DELETE.value in request.POST: 

907 return self.remove_author(request) 

908 elif self._FORM_ACTION_CORRESPONDING in request.POST: 

909 return self.toggle_primary_author(request) 

910 

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

912 

913 def add_author(self, request, *args, **kwargs): 

914 """ 

915 Add an `SubmissionAuthor` to the submission. 

916 """ 

917 response = HttpResponseRedirect( 

918 reverse("mesh:submission_authors", kwargs={"submission_pk": self.submission.pk}) 

919 ) 

920 form = SubmissionAuthorForm(request.POST, submission=self.submission) 

921 if not form.is_valid(): 

922 self.init_form = form 

923 return self.get(request, *args, **kwargs) 

924 

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

926 form.instance.submission = self.submission 

927 form.save() 

928 

929 SubmissionLog.add_message( 

930 self.submission, 

931 content=f"Author added: {form.instance.first_name} {form.instance.last_name} ({form.instance.email})", 

932 content_en=_("Author added") 

933 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})", 

934 user=request.user, 

935 significant=True, 

936 ) 

937 messages.success( 

938 request, 

939 _("Author added") 

940 + f": {form.instance.first_name} {form.instance.last_name} ({form.instance.email})", 

941 ) 

942 

943 return response 

944 

945 def remove_author(self, request: HttpRequest) -> HttpResponse: 

946 """ 

947 Remove a `SubmissionAuthor` from the submission. 

948 """ 

949 response = HttpResponseRedirect( 

950 reverse("mesh:submission_authors", kwargs={"submission_pk": self.submission.pk}) 

951 ) 

952 form = HiddenModelChoiceForm(request.POST, _queryset=self.submission.authors_censored) 

953 if not form.is_valid(): 

954 messages.error(request, _("Something went wrong. Please try again.")) 

955 return response 

956 

957 if len(self.submission.authors_censored) < 2: 

958 messages.error( 

959 request, 

960 _("There must be at least 1 author attached to the submission."), 

961 ) 

962 return response 

963 

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

965 author_string = str(author) 

966 author.delete() 

967 

968 SubmissionLog.add_message( 

969 self.submission, 

970 content=f"Author removed: {author_string}", 

971 content_en=_("Author added") + f": {author_string}", 

972 request=request.user, 

973 significant=True, 

974 ) 

975 messages.success(request, _("The author has been removed.")) 

976 return response 

977 

978 def toggle_primary_author(self, request: HttpRequest) -> HttpResponse: 

979 """ 

980 Toggle the `corresponding` boolean of a `SubmissionAuthor` from the submission. 

981 """ 

982 response = HttpResponseRedirect( 

983 reverse("mesh:submission_authors", kwargs={"submission_pk": self.submission.pk}) 

984 ) 

985 form = HiddenModelChoiceForm(request.POST, _queryset=self.submission.authors_censored) 

986 if not form.is_valid(): 

987 messages.error(request, _("Something went wrong. Please try again.")) 

988 return response 

989 

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

991 author.corresponding = not author.corresponding 

992 author.save() 

993 word = "marked" if author.corresponding else "unmarked" 

994 messages.success( 

995 request, 

996 _(f"Author {author} has been {word} as a corresponding contact."), 

997 ) 

998 

999 return response 

1000 

1001 

1002class SubmissionConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView): 

1003 """ 

1004 View to confirm the submission the current SubmissionVersion. 

1005 

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

1007 submitting a revised SubmissionVersion. 

1008 """ 

1009 

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

1011 submission: Submission 

1012 form_class = SubmissionConfirmForm 

1013 

1014 def setup(self, request, *args, **kwargs): 

1015 super().setup(request, *args, **kwargs) 

1016 self.submission = self.get_submission() 

1017 if not self.request.current_role.can_edit_submission(self.submission): 

1018 raise PermissionError 

1019 

1020 def get_success_url(self): 

1021 return reverse("mesh:submission_details", kwargs={"submission_pk": self.submission.pk}) 

1022 

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

1024 if not self.submission.is_submittable(): 

1025 # TODO : Enhance error message 

1026 # We theorically shouldn't even be able to get there with an invalid submission 

1027 # But real examples have shown that it can happen 

1028 messages.error( 

1029 request, 

1030 _("There must be at least 1 author attached to the submission."), 

1031 ) 

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

1033 

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

1035 return self.submission.get_current_version() 

1036 

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

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

1039 

1040 context["form"].buttons = [ 

1041 Button( 

1042 id="form_save", 

1043 title=_("Submit"), 

1044 icon_class="fa-check", 

1045 attrs={ 

1046 "type": ["submit"], 

1047 "class": ["save-button", "button-highlight"], 

1048 }, 

1049 ) 

1050 ] 

1051 

1052 if self.submission.is_draft: 

1053 stepper = get_submission_stepper(self.submission) 

1054 stepper.set_active_step("confirm") 

1055 context["stepper"] = stepper 

1056 

1057 context["submission_proxy"] = SubmissionProxy(self.submission, self.request.current_role) 

1058 

1059 files = [] 

1060 files.append( 

1061 { 

1062 "file_wrapper": self.submission.get_current_version().main_file, 

1063 "type": "Main", 

1064 } 

1065 ) 

1066 

1067 for file_wrapper in self.submission.get_current_version().additional_files.all(): 

1068 files.append({"file_wrapper": file_wrapper, "type": "Additional"}) 

1069 context["submission_files"] = files 

1070 

1071 # # Breadcrumb 

1072 # breadcrumb = get_submission_breadcrumb(self.submission) 

1073 # breadcrumb.add_item( 

1074 # title=_("Authors"), 

1075 # url=reverse("mesh:submission_confirm", kwargs={"pk": self.submission.pk}), 

1076 # ) 

1077 # context["breadcrumb"] = breadcrumb 

1078 

1079 return context 

1080 

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

1082 """ 

1083 Submit the submission's current version. 

1084 """ 

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

1086 

1087 if self.submission.get_current_version().number == 1: 

1088 messages.success( 

1089 self.request, 

1090 _("Your submission has been successfully saved and confirmed."), 

1091 ) 

1092 else: 

1093 messages.success(self.request, _("Your revisions were successfully submitted.")) 

1094 

1095 previous_round_number = self.submission.get_current_version().number - 1 

1096 if previous_round_number > 0: 

1097 previous_round = self.submission.versions.get(number=previous_round_number) 

1098 previous_reviewers = [ 

1099 review.reviewer 

1100 for review in previous_round.reviews.filter(state=ReviewState.SUBMITTED.value) 

1101 ] 

1102 for previous_reviewer in previous_reviewers: 

1103 qs = Suggestion.objects.filter( 

1104 submission=self.submission, suggested_user=previous_reviewer 

1105 ) 

1106 if not qs.exists(): 

1107 add_suggestion( 

1108 submission=self.submission, 

1109 suggested_user=previous_reviewer, 

1110 suggested_reviewer=None, 

1111 ) 

1112 

1113 return HttpResponseRedirect(self.get_success_url()) 

1114 

1115 

1116class SubmissionDeleteView(LoginRequiredMixin, MeshObjectMixin, DeleteView): 

1117 model = Submission 

1118 submission: Submission 

1119 form_class = SubmissionDeleteForm 

1120 template_name = "mesh/forms/submission_delete.html" 

1121 restricted_roles = [JournalManager, Author] 

1122 message_on_restrict = False 

1123 

1124 def setup(self, request, *args, **kwargs): 

1125 super().setup(request, *args, **kwargs) 

1126 self.submission = self.get_submission() 

1127 if not self.request.current_role.can_edit_submission(self.submission): 

1128 raise PermissionError 

1129 

1130 def get_object(self, *args, **kwargs) -> Submission: 

1131 """ 

1132 Override `get_object` built-in method to avoid an additional look-up when 

1133 we already have the object loaded. 

1134 """ 

1135 return self.submission 

1136 

1137 def get_success_url(self): 

1138 return reverse("mesh:submission_list") 

1139 

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

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

1142 

1143 context["submission"] = self.submission 

1144 context["form"].buttons = [ 

1145 Button( 

1146 id="form_save", 

1147 title=_("Delete"), 

1148 icon_class="fa-trash", 

1149 attrs={ 

1150 "type": ["submit"], 

1151 "class": ["save-button", "button-highlight"], 

1152 }, 

1153 ) 

1154 ] 

1155 

1156 return context