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

503 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-09-11 12:27 +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.validation = { 

229 "required_checks": ["contributors", "abstract", "title", "pdf"], 

230 "blocking_checks": [], 

231 } 

232 

233 self.submission = None 

234 self.version = None 

235 self.submission_main_file = None 

236 

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

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

239 self.submission = self.get_submission() 

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

241 raise PermissionError 

242 

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

244 colid = app_settings.COLID 

245 data_article = create_articledata() 

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

247 data["editorial_tools"] = self.editorial_tools 

248 

249 def obj_to_dict(obj): 

250 return obj.__dict__ 

251 

252 data["article"]["colid"] = colid 

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

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

255 

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

257 if method == "get": 

258 submission_main_file = getattr( 

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

260 ) 

261 if submission_main_file is not None: 

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

263 else: 

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

265 

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

267 submission_main_file = getattr( 

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

269 ) 

270 if submission_main_file is not None: 

271 submission_main_file.delete() 

272 

273 submission_main_file = SubmissionMainFile( 

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

275 ) 

276 submission_main_file.save() 

277 

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

279 if method == "get": 

280 data["article"]["titles"] = [ 

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

282 ] 

283 else: 

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

285 

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

287 if method == "get": 

288 contributors = [] 

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

290 contrib = create_contributor() 

291 contrib["first_name"] = author.first_name 

292 contrib["last_name"] = author.last_name 

293 contrib["email"] = author.email 

294 contrib["corresponding"] = author.corresponding 

295 contributors.append(contrib) 

296 data["article"]["contributors"] = contributors 

297 else: 

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

299 

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

301 if method == "get": 

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

303 else: 

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

305 

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

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

308 data = json.loads(body_unicode) 

309 

310 if ( 

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

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

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

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

315 ): 

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

317 

318 data["colid"] = app_settings.COLID 

319 

320 article_data = self.convert_data_from_vuejs3(data) 

321 title = article_data.title_html 

322 

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

324 

325 self.submission.name = title 

326 self.submission.abstract = abstract 

327 self.submission._user = request.user 

328 self.submission.save() 

329 

330 current_version = self.submission.get_current_version() 

331 current_version._user = request.user 

332 current_version.save() 

333 

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

335 

336 for contrib in article_data.contributors: 

337 author = SubmissionAuthor( 

338 submission=self.submission, 

339 first_name=contrib["first_name"], 

340 last_name=contrib["last_name"], 

341 email=contrib["email"], 

342 corresponding=contrib["corresponding"], 

343 ) 

344 author.save() 

345 

346 data["redirect"] = { 

347 "redirect": True, 

348 "url": reverse( 

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

350 ), 

351 } 

352 

353 return JsonResponse(data) 

354 

355 

356class SubmissionRedirectFromVue(TemplateView): 

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

358 

359 def get_context_data(self, **kwargs): 

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

361 context["message"] = ( 

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

363 ) 

364 return context 

365 

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

367 """ 

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

369 """ 

370 

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

372 

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

374 

375 

376class SubmissionInfoView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

377 """ 

378 View for updating the submission info. 

379 """ 

380 

381 model = Submission 

382 form_class = SubmissionInfoForm 

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

384 submission: Submission 

385 restricted_roles = [Author] 

386 message_on_restrict = False 

387 

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

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

390 self.submission = self.get_submission() 

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

392 raise PermissionError 

393 

394 def get_success_url(self) -> str: 

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

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

397 

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

399 

400 def get_fail_redirect_uri(self) -> str: 

401 """ 

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

403 """ 

404 return self.get_success_url() 

405 

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

407 """ 

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

409 we already have the object loaded. 

410 """ 

411 return self.submission 

412 

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

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

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

416 

417 if self.submission.is_draft: 

418 step_id = "info" 

419 stepper = get_submission_stepper(self.submission) 

420 stepper.set_active_step(step_id) 

421 context["stepper"] = stepper 

422 

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

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

425 

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

427 for suggestion in suggestions: 

428 reviewer = ( 

429 suggestion.suggested_reviewer 

430 if suggestion.suggested_reviewer is not None 

431 else suggestion.suggested_user 

432 ) 

433 if suggestion.suggest_to_avoid: 

434 context["avoid_reviewer"] = reviewer 

435 else: 

436 context["suggested_reviewer"] = reviewer 

437 

438 return context 

439 

440 def form_valid(self, form): 

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

442 

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

444 

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

446 person_info = { 

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

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

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

450 } 

451 

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

453 add_suggestion_from_person( 

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

455 ) 

456 

457 return super().form_valid(form) 

458 

459 

460class SubmissionEditArticleMetadataView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

461 """ 

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

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

464 The POST is handled by SubmissionEditArticleMetadataVuejsAPIView 

465 """ 

466 

467 model = Submission 

468 form_class = SubmissionEditArticleMetadataForm 

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

470 submission: Submission 

471 restricted_roles = [Author] 

472 message_on_restrict = False 

473 

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

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

476 self.submission = self.get_submission() 

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

478 raise PermissionError 

479 

480 # def get_fail_redirect_uri(self) -> str: 

481 # """ 

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

483 # Not used with VueJS 

484 # """ 

485 # return self.get_success_url() 

486 

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

488 """ 

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

490 we already have the object loaded. 

491 """ 

492 return self.submission 

493 

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

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

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

497 

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

499 context["data_get_url"] = reverse( 

500 "mesh:submission_update_vuejs", 

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

502 ) 

503 

504 if self.submission.is_draft: 

505 step_id = "metadata" 

506 stepper = get_submission_stepper(self.submission) 

507 stepper.set_active_step(step_id) 

508 context["stepper"] = stepper 

509 

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

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

512 

513 # Generate breadcrumb data 

514 # breadcrumb = get_submission_breadcrumb(self.submission) 

515 # breadcrumb.add_item( 

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

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

518 # ) 

519 # context["breadcrumb"] = breadcrumb 

520 return context 

521 

522 def get_success_url(self) -> str: 

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

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

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

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

527 

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

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

530 # if not form.changed_data: 

531 # form.instance.do_not_update = True 

532 # 

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

534 # 

535 # if form.has_changed(): 

536 # SubmissionLog.add_message( 

537 # self.submission, 

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

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

540 # user=self.request.user, 

541 # ) 

542 # return response 

543 

544 

545class SubmissionVersionCreateView( 

546 LoginRequiredMixin, MeshObjectMixin, SubmittableModelFormMixin, CreateView 

547): 

548 """ 

549 View for creating a new submission version. 

550 """ 

551 

552 model = SubmissionVersion 

553 form_class = SubmissionVersionForm 

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

555 submission: Submission 

556 restricted_roles = [Author] 

557 save_submission = False 

558 add_confirm_message = False 

559 

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

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

562 self.submission = self.get_submission() 

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

564 raise PermissionError 

565 

566 def get_form_kwargs(self): 

567 kwargs = super().get_form_kwargs() 

568 kwargs["submission"] = self.submission 

569 return kwargs 

570 

571 def form_valid(self, form): 

572 form.instance.submission = self.submission 

573 return super().form_valid(form) 

574 

575 def get_success_url(self) -> str: 

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

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

578 return self.submit_url() 

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

580 

581 def submit_url(self) -> str: 

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

583 

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

585 # Not used with VueJS 

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

587 # form.instance.submission = self.submission 

588 

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

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

591 current_version = self.submission.get_current_version() 

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

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

594 if not self.submission.is_draft: 

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

596 

597 submission_url = reverse( 

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

599 ) 

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

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

602 context["form_description"] = [description] 

603 

604 if self.submission.is_draft: 

605 stepper = get_submission_stepper(self.submission) 

606 stepper.set_active_step("version") 

607 context["stepper"] = stepper 

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

609 

610 # # Generate breadcrumb data 

611 # breadcrumb = get_submission_breadcrumb(self.submission) 

612 # breadcrumb.add_item( 

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

614 # url=reverse_lazy( 

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

616 # ), 

617 # ) 

618 # context["breadcrumb"] = breadcrumb 

619 return context 

620 

621 # def form_valid(self, form): 

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

623 # return result 

624 

625 

626class SubmissionVersionUpdateView( 

627 LoginRequiredMixin, MeshObjectMixin, SubmittableModelFormMixin, UpdateView 

628): 

629 """ 

630 View for updating a submission version. 

631 

632 Submitting a version = submitting the whole submission. 

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

634 taken when this is the first version. 

635 """ 

636 

637 model = SubmissionVersion 

638 form_class = SubmissionVersionForm 

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

640 

641 restricted_roles = [Author] 

642 message_on_restrict = False 

643 save_submission = False 

644 add_confirm_message = False 

645 

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

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

648 self.version = self.get_version() 

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

650 raise PermissionError 

651 

652 def get_fail_redirect_uri(self) -> str: 

653 """ 

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

655 """ 

656 return self.get_success_url() 

657 

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

659 """ 

660 Returns the already fetched version. 

661 """ 

662 return self.version 

663 

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

665 kwargs = super().get_form_kwargs() 

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

667 kwargs[SUBMIT_QUERY_PARAMETER] = True 

668 return kwargs 

669 

670 def get_success_url(self) -> str: 

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

672 return self.submit_url() 

673 return reverse( 

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

675 ) 

676 

677 def submit_url(self) -> str: 

678 return reverse( 

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

680 ) 

681 

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

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

684 

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

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

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

688 

689 submission_url = reverse( 

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

691 ) 

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

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

694 context["form_description"] = [description] 

695 

696 if self.version.submission.is_draft: 

697 stepper = get_submission_stepper(self.version.submission) 

698 stepper.set_active_step("version") 

699 context["stepper"] = stepper 

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

701 

702 # # Generate breadcrumb data 

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

704 # breadcrumb.add_item( 

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

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

707 # ) 

708 # context["breadcrumb"] = breadcrumb 

709 return context 

710 

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

712 """ 

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

714 """ 

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

716 

717 if deletion_requested: 

718 # Update the submission object. 

719 self.version = self.get_version() 

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

721 

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

723 

724 

725class SubmissionResumeView(LoginRequiredMixin, MeshObjectMixin, View): 

726 """ 

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

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

729 

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

731 the submission status. 

732 - STEP 1: Preprint id 

733 - STEP 2: Article Metadata 

734 - STEP 3: Submission Info 

735 - STEP 4: Confirmation 

736 """ 

737 

738 submission: Submission 

739 restricted_roles = [Author] 

740 

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

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

743 self.submission = self.get_submission() 

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

745 raise PermissionError 

746 

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

748 stepper = get_submission_stepper(self.submission) 

749 

750 # Redirects to the last active step 

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

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

753 if step.href and step.can_navigate: 

754 return HttpResponseRedirect(step.href) 

755 

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

757 

758 

759class SubmissionAuthorView(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

760 """ 

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

762 """ 

763 

764 submission: Submission 

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

766 restricted_roles = [Author] 

767 init_form: SubmissionAuthorForm | None = None 

768 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding" 

769 

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

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

772 self.submission = self.get_submission() 

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

774 raise PermissionError 

775 

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

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

778 

779 context["submission"] = self.submission 

780 

781 authors_qs = self.submission.authors.all() 

782 context["authors"] = [ 

783 { 

784 "author": author, 

785 "delete_form": HiddenModelChoiceForm( 

786 form_action=FormAction.DELETE.value, 

787 initial={"instance": author}, 

788 _queryset=authors_qs, 

789 ), 

790 "corresponding_form": HiddenModelChoiceForm( 

791 form_action="_action_toggle_corresponding", 

792 initial={"instance": author}, 

793 _queryset=authors_qs, 

794 ), 

795 "buttons": [ 

796 Button( 

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

798 title=_("Corresponding"), 

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

800 form=HiddenModelChoiceForm( 

801 initial={"instance": author}, _queryset=authors_qs 

802 ), 

803 attrs={ 

804 "href": [ 

805 reverse( 

806 "mesh:submission_authors", 

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

808 ) 

809 ], 

810 "type": ["submit"], 

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

812 "name": [self._FORM_ACTION_CORRESPONDING], 

813 "data-tooltip": [ 

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

815 ], 

816 }, 

817 ), 

818 Button( 

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

820 title=_("Remove"), 

821 icon_class="fa-trash", 

822 form=HiddenModelChoiceForm( 

823 initial={"instance": author}, _queryset=authors_qs 

824 ), 

825 attrs={ 

826 "href": [ 

827 reverse( 

828 "mesh:submission_authors", 

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

830 ) 

831 ], 

832 "type": ["submit"], 

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

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

835 }, 

836 ), 

837 ], 

838 } 

839 for author in self.submission.authors_censored 

840 ] 

841 

842 initial = {} 

843 if not self.submission.authors_censored: 

844 initial.update( 

845 { 

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

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

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

849 "primary": True, 

850 } 

851 ) 

852 

853 form = self.init_form or SubmissionAuthorForm( 

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

855 ) 

856 

857 form.buttons = [ 

858 Button( 

859 id="form_save", 

860 title=_("Add author"), 

861 icon_class="fa-plus", 

862 attrs={"type": ["submit"], "class": ["btn-save-form"]}, 

863 ) 

864 ] 

865 

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

867 

868 if self.submission.is_draft: 

869 step_id = "authors" 

870 stepper = get_submission_stepper(self.submission) 

871 stepper.set_active_step(step_id) 

872 context["stepper"] = stepper 

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

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

875 # button = Button( 

876 # id="next", 

877 # title=_("Next"), 

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

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

880 # ) 

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

882 # 

883 # form.buttons.append( 

884 # Button( 

885 # id="form_next", 

886 # title=_("Next"), 

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

888 # attrs={ 

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

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

891 # "class": ["as-button", "btn-save-form"], 

892 # }, 

893 # ) 

894 # ) 

895 

896 context["form"] = form 

897 

898 # # Generate breadcrumb data 

899 # breadcrumb = get_submission_breadcrumb(self.submission) 

900 # breadcrumb.add_item( 

901 # title=_("Authors"), 

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

903 # ) 

904 # context["breadcrumb"] = breadcrumb 

905 

906 return context 

907 

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

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

910 return self.remove_author(request) 

911 elif self._FORM_ACTION_CORRESPONDING in request.POST: 

912 return self.toggle_primary_author(request) 

913 

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

915 

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

917 """ 

918 Add an `SubmissionAuthor` to the submission. 

919 """ 

920 response = HttpResponseRedirect( 

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

922 ) 

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

924 if not form.is_valid(): 

925 self.init_form = form 

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

927 

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

929 form.instance.submission = self.submission 

930 form.save() 

931 

932 SubmissionLog.add_message( 

933 self.submission, 

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

935 content_en=_("Author added") 

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

937 user=request.user, 

938 significant=True, 

939 ) 

940 messages.success( 

941 request, 

942 _("Author added") 

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

944 ) 

945 

946 return response 

947 

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

949 """ 

950 Remove a `SubmissionAuthor` from the submission. 

951 """ 

952 response = HttpResponseRedirect( 

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

954 ) 

955 form = HiddenModelChoiceForm(request.POST, _queryset=self.submission.authors.all()) 

956 if not form.is_valid(): 

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

958 return response 

959 

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

961 messages.error( 

962 request, 

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

964 ) 

965 return response 

966 

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

968 author_string = str(author) 

969 author.delete() 

970 

971 SubmissionLog.add_message( 

972 self.submission, 

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

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

975 user=request.user, 

976 significant=True, 

977 ) 

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

979 return response 

980 

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

982 """ 

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

984 """ 

985 response = HttpResponseRedirect( 

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

987 ) 

988 form = HiddenModelChoiceForm(request.POST, _queryset=self.submission.authors.all()) 

989 if not form.is_valid(): 

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

991 return response 

992 

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

994 author.corresponding = not author.corresponding 

995 author.save() 

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

997 messages.success( 

998 request, 

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

1000 ) 

1001 

1002 return response 

1003 

1004 

1005class SubmissionConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView): 

1006 """ 

1007 View to confirm the submission the current SubmissionVersion. 

1008 

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

1010 submitting a revised SubmissionVersion. 

1011 """ 

1012 

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

1014 submission: Submission 

1015 form_class = SubmissionConfirmForm 

1016 

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

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

1019 self.submission = self.get_submission() 

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

1021 raise PermissionError 

1022 

1023 def get_success_url(self): 

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

1025 

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

1027 if not self.submission.is_submittable(): 

1028 # TODO : Enhance error message 

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

1030 # But real examples have shown that it can happen 

1031 messages.error( 

1032 request, 

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

1034 ) 

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

1036 

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

1038 return self.submission.get_current_version() 

1039 

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

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

1042 

1043 context["form"].buttons = [ 

1044 Button( 

1045 id="form_save", 

1046 title=_("Submit"), 

1047 icon_class="fa-check", 

1048 attrs={ 

1049 "type": ["submit"], 

1050 "class": ["btn-save-form", "button-highlight"], 

1051 }, 

1052 ) 

1053 ] 

1054 

1055 if self.submission.is_draft: 

1056 stepper = get_submission_stepper(self.submission) 

1057 stepper.set_active_step("confirm") 

1058 context["stepper"] = stepper 

1059 

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

1061 

1062 files = [] 

1063 files.append( 

1064 { 

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

1066 "type": "Main", 

1067 } 

1068 ) 

1069 

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

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

1072 context["submission_files"] = files 

1073 

1074 # # Breadcrumb 

1075 # breadcrumb = get_submission_breadcrumb(self.submission) 

1076 # breadcrumb.add_item( 

1077 # title=_("Authors"), 

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

1079 # ) 

1080 # context["breadcrumb"] = breadcrumb 

1081 

1082 return context 

1083 

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

1085 """ 

1086 Submit the submission's current version. 

1087 """ 

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

1089 

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

1091 messages.success( 

1092 self.request, 

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

1094 ) 

1095 else: 

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

1097 

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

1099 if previous_round_number > 0: 

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

1101 previous_reviewers = [ 

1102 review.reviewer 

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

1104 ] 

1105 for previous_reviewer in previous_reviewers: 

1106 qs = Suggestion.objects.filter( 

1107 submission=self.submission, suggested_user=previous_reviewer 

1108 ) 

1109 if not qs.exists(): 

1110 add_suggestion( 

1111 submission=self.submission, 

1112 suggested_user=previous_reviewer, 

1113 suggested_reviewer=None, 

1114 ) 

1115 

1116 return HttpResponseRedirect(self.get_success_url()) 

1117 

1118 

1119class SubmissionDeleteView(LoginRequiredMixin, MeshObjectMixin, DeleteView): 

1120 model = Submission 

1121 submission: Submission 

1122 form_class = SubmissionDeleteForm 

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

1124 restricted_roles = [JournalManager, Author] 

1125 message_on_restrict = False 

1126 

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

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

1129 self.submission = self.get_submission() 

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

1131 raise PermissionError 

1132 

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

1134 """ 

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

1136 we already have the object loaded. 

1137 """ 

1138 return self.submission 

1139 

1140 def get_success_url(self): 

1141 return reverse("mesh:submission_list") 

1142 

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

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

1145 

1146 context["submission"] = self.submission 

1147 context["form"].buttons = [ 

1148 Button( 

1149 id="form_save", 

1150 title=_("Delete"), 

1151 icon_class="fa-trash", 

1152 attrs={ 

1153 "type": ["submit"], 

1154 "class": ["btn-save-form", "button-highlight"], 

1155 }, 

1156 ) 

1157 ] 

1158 

1159 return context