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

504 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-09-10 11:20 +0000

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 matching import crossref 

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 = crossref.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 = crossref.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 

359 self.submission = None 

360 self.version = None 

361 self.submission_main_file = None 

362 

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

364 pk = kwargs["pk"] 

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

366 

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

368 

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

370 colid = app_settings.COLID 

371 data_article = create_articledata() 

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

373 data["editorial_tools"] = self.editorial_tools 

374 

375 def obj_to_dict(obj): 

376 return obj.__dict__ 

377 

378 data["article"]["colid"] = colid 

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

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

381 

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

383 if method == "get": 

384 submission_main_file = getattr(self.submission.current_version, "main_file", None) 

385 if submission_main_file is not None: 

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

387 else: 

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

389 

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

391 submission_main_file = getattr(self.submission.current_version, "main_file", None) 

392 if submission_main_file is not None: 

393 submission_main_file.delete() 

394 

395 submission_main_file = SubmissionMainFile( 

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

397 ) 

398 submission_main_file.save() 

399 

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

401 if method == "get": 

402 data["article"]["titles"] = [ 

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

404 ] 

405 else: 

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

407 

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

409 if method == "get": 

410 contributors = [] 

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

412 contrib = create_contributor() 

413 contrib["first_name"] = author.first_name 

414 contrib["last_name"] = author.last_name 

415 contrib["email"] = author.email 

416 contrib["corresponding"] = author.corresponding 

417 contributors.append(contrib) 

418 data["article"]["contributors"] = contributors 

419 else: 

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

421 

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

423 if method == "get": 

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

425 else: 

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

427 

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

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

430 data = json.loads(body_unicode) 

431 

432 if ( 

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

434 or data["abstracts"][0]["value_html"] == "" 

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

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

437 ): 

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

439 

440 data["colid"] = app_settings.COLID 

441 

442 article_data = self.convert_data_from_vuejs3(data) 

443 title = article_data.titles[0]["title_html"] 

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

445 

446 self.submission.name = title 

447 self.submission.abstract = abstract 

448 self.submission._user = request.user 

449 self.submission.save() 

450 

451 current_version = self.submission.current_version 

452 current_version._user = request.user 

453 current_version.save() 

454 

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

456 

457 for contrib in article_data.contributors: 

458 author = SubmissionAuthor( 

459 submission=self.submission, 

460 first_name=contrib["first_name"], 

461 last_name=contrib["last_name"], 

462 email=contrib["email"], 

463 corresponding=contrib["corresponding"], 

464 ) 

465 author.save() 

466 

467 data["redirect"] = { 

468 "redirect": True, 

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

470 } 

471 

472 return JsonResponse(data) 

473 

474 

475class SubmissionRedirectFromVue(TemplateView): 

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

477 

478 def get_context_data(self, **kwargs): 

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

480 context["message"] = ( 

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

482 ) 

483 return context 

484 

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

486 """ 

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

488 """ 

489 

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

491 

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

493 

494 

495class SubmissionInfoView(RoleMixin, UpdateView): 

496 """ 

497 View for updating the submission info. 

498 """ 

499 

500 model: type[Submission] = Submission 

501 form_class = SubmissionInfoForm 

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

503 submission: Submission 

504 restricted_roles = [Author] 

505 message_on_restrict = False 

506 

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

508 pk = kwargs["pk"] 

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

510 

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

512 

513 def get_success_url(self) -> str: 

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

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

516 

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

518 

519 def get_fail_redirect_uri(self) -> str: 

520 """ 

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

522 """ 

523 return self.get_success_url() 

524 

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

526 """ 

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

528 we already have the object loaded. 

529 """ 

530 return self.submission 

531 

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

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

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

535 

536 if self.submission.is_draft: 

537 step_id = "info" 

538 stepper = get_submission_stepper(self.submission) 

539 stepper.set_active_step(step_id) 

540 context["stepper"] = stepper 

541 

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

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

544 

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

546 for suggestion in suggestions: 

547 reviewer = ( 

548 suggestion.suggested_reviewer 

549 if suggestion.suggested_reviewer is not None 

550 else suggestion.suggested_user 

551 ) 

552 if suggestion.suggest_to_avoid: 

553 context["avoid_reviewer"] = reviewer 

554 else: 

555 context["suggested_reviewer"] = reviewer 

556 

557 return context 

558 

559 def form_valid(self, form): 

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

561 

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

563 

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

565 person_info = { 

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

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

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

569 } 

570 

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

572 add_suggestion_from_person( 

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

574 ) 

575 

576 return super().form_valid(form) 

577 

578 

579class SubmissionEditArticleMetadataView(RoleMixin, UpdateView): 

580 """ 

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

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

583 The POST is handled by SubmissionEditArticleMetadataVuejsAPIView 

584 """ 

585 

586 model: type[Submission] = Submission 

587 form_class = SubmissionEditArticleMetadataForm 

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

589 submission: Submission 

590 restricted_roles = [Author] 

591 message_on_restrict = False 

592 

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

594 pk = kwargs["pk"] 

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

596 

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

598 

599 # def get_fail_redirect_uri(self) -> str: 

600 # """ 

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

602 # Not used with VueJS 

603 # """ 

604 # return self.get_success_url() 

605 

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

607 """ 

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

609 we already have the object loaded. 

610 """ 

611 return self.submission 

612 

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

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

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

616 

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

618 context["data_get_url"] = reverse_lazy( 

619 "mesh:submission_update_vuejs", 

620 kwargs={"pk": self.submission.pk}, 

621 ) 

622 

623 if self.submission.is_draft: 

624 step_id = "metadata" 

625 stepper = get_submission_stepper(self.submission) 

626 stepper.set_active_step(step_id) 

627 context["stepper"] = stepper 

628 

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

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

631 

632 # Generate breadcrumb data 

633 # breadcrumb = get_submission_breadcrumb(self.submission) 

634 # breadcrumb.add_item( 

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

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

637 # ) 

638 # context["breadcrumb"] = breadcrumb 

639 return context 

640 

641 def get_success_url(self) -> str: 

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

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

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

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

646 

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

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

649 # if not form.changed_data: 

650 # form.instance.do_not_update = True 

651 # 

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

653 # 

654 # if form.has_changed(): 

655 # SubmissionLog.add_message( 

656 # self.submission, 

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

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

659 # user=self.request.user, 

660 # ) 

661 # return response 

662 

663 

664class SubmissionVersionCreateView(RoleMixin, SubmittableModelFormMixin, CreateView): 

665 """ 

666 View for creating a new submission version. 

667 """ 

668 

669 model = SubmissionVersion 

670 form_class = SubmissionVersionForm 

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

672 submission: Submission 

673 restricted_roles = [Author] 

674 save_submission = False 

675 add_confirm_message = False 

676 

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

678 submission_pk = kwargs["submission_pk"] 

679 self.submission = get_object_or_404(Submission, pk=submission_pk) 

680 

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

682 

683 def get_success_url(self) -> str: 

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

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

686 return self.submit_url() 

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

688 

689 def submit_url(self) -> str: 

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

691 

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

693 # Not used with VueJS 

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

695 # form.instance.submission = self.submission 

696 

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

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

699 current_version = self.submission.current_version 

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

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

702 if not self.submission.is_draft: 

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

704 

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

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

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

708 context["form_description"] = [description] 

709 

710 if self.submission.is_draft: 

711 stepper = get_submission_stepper(self.submission) 

712 stepper.set_active_step("version") 

713 context["stepper"] = stepper 

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

715 

716 # # Generate breadcrumb data 

717 # breadcrumb = get_submission_breadcrumb(self.submission) 

718 # breadcrumb.add_item( 

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

720 # url=reverse_lazy( 

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

722 # ), 

723 # ) 

724 # context["breadcrumb"] = breadcrumb 

725 return context 

726 

727 # def form_valid(self, form): 

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

729 # return result 

730 

731 

732class SubmissionVersionUpdateView(RoleMixin, SubmittableModelFormMixin, UpdateView): 

733 """ 

734 View for updating a submission version. 

735 

736 Submitting a version = submitting the whole submission. 

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

738 taken when this is the first version. 

739 """ 

740 

741 model: type[SubmissionVersion] = SubmissionVersion 

742 form_class = SubmissionVersionForm 

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

744 version: SubmissionVersion 

745 restricted_roles = [Author] 

746 message_on_restrict = False 

747 save_submission = False 

748 add_confirm_message = False 

749 

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

751 pk = kwargs["pk"] 

752 self.version = get_object_or_404(self.model, pk=pk) 

753 return not self.role_handler.check_rights("can_edit_version", self.version) 

754 

755 def get_fail_redirect_uri(self) -> str: 

756 """ 

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

758 """ 

759 return self.get_success_url() 

760 

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

762 """ 

763 Returns the already fetched version. 

764 """ 

765 return self.version 

766 

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

768 kwargs = super().get_form_kwargs() 

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

770 kwargs[SUBMIT_QUERY_PARAMETER] = True 

771 return kwargs 

772 

773 def get_success_url(self) -> str: 

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

775 return self.submit_url() 

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

777 

778 def submit_url(self) -> str: 

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

780 

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

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

783 

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

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

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

787 

788 submission_url = reverse_lazy( 

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

790 ) 

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

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

793 context["form_description"] = [description] 

794 

795 if self.version.submission.is_draft: 

796 stepper = get_submission_stepper(self.version.submission) 

797 stepper.set_active_step("version") 

798 context["stepper"] = stepper 

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

800 

801 # # Generate breadcrumb data 

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

803 # breadcrumb.add_item( 

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

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

806 # ) 

807 # context["breadcrumb"] = breadcrumb 

808 return context 

809 

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

811 """ 

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

813 """ 

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

815 

816 if deletion_requested: 

817 # Update the submission object. 

818 self.version = get_object_or_404(self.model, pk=self.version.pk) 

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

820 

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

822 

823 

824class SubmissionResumeView(RoleMixin, View): 

825 """ 

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

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

828 

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

830 the submission status. 

831 - STEP 1: Preprint id 

832 - STEP 2: Article Metadata 

833 - STEP 3: Submission Info 

834 - STEP 4: Confirmation 

835 """ 

836 

837 submission: Submission 

838 restricted_roles = [Author] 

839 

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

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

842 return not self.role_handler.check_rights("can_submit_submission", self.submission) 

843 

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

845 stepper = get_submission_stepper(self.submission) 

846 

847 # Redirects to the last active step 

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

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

850 if step.href and step.can_navigate: 

851 return HttpResponseRedirect(step.href) 

852 

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

854 

855 

856class SubmissionAuthorView(RoleMixin, TemplateView): 

857 """ 

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

859 """ 

860 

861 submission: Submission 

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

863 restricted_roles = [Author] 

864 init_form: SubmissionAuthorForm | None = None 

865 _FORM_ACTION_CORRESPONDING = "_action_toggle_corresponding" 

866 

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

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

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

870 

871 @cached_property 

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

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

874 

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

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

877 

878 context["submission"] = self.submission 

879 

880 authors = self.authors 

881 context["authors"] = [ 

882 { 

883 "author": author, 

884 "delete_form": HiddenModelChoiceForm( 

885 _queryset=authors, 

886 form_action=FormAction.DELETE.value, 

887 initial={"instance": author}, 

888 ), 

889 "corresponding_form": HiddenModelChoiceForm( 

890 _queryset=authors, 

891 form_action="_action_toggle_corresponding", 

892 initial={"instance": author}, 

893 ), 

894 "buttons": [ 

895 Button( 

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

897 title=_("Corresponding"), 

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

899 form=HiddenModelChoiceForm( 

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

901 ), 

902 attrs={ 

903 "href": [ 

904 reverse_lazy( 

905 "mesh:submission_authors", 

906 kwargs={"pk": self.submission.pk}, 

907 ) 

908 ], 

909 "type": ["submit"], 

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

911 "name": [self._FORM_ACTION_CORRESPONDING], 

912 "data-tooltip": [ 

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

914 ], 

915 }, 

916 ), 

917 Button( 

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

919 title=_("Remove"), 

920 icon_class="fa-trash", 

921 form=HiddenModelChoiceForm( 

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

923 ), 

924 attrs={ 

925 "href": [ 

926 reverse_lazy( 

927 "mesh:submission_authors", 

928 kwargs={"pk": self.submission.pk}, 

929 ) 

930 ], 

931 "type": ["submit"], 

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

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

934 }, 

935 ), 

936 ], 

937 } 

938 for author in authors 

939 ] 

940 

941 initial = {} 

942 if not authors: 

943 initial.update( 

944 { 

945 "first_name": self.request.user.first_name, # type:ignore 

946 "last_name": self.request.user.last_name, # type:ignore 

947 "email": self.request.user.email, # type:ignore 

948 "primary": True, 

949 } 

950 ) 

951 

952 form = self.init_form or SubmissionAuthorForm( 

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

954 ) 

955 

956 form.buttons = [ 

957 Button( 

958 id="form_save", 

959 title=_("Add author"), 

960 icon_class="fa-plus", 

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

962 ) 

963 ] 

964 

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

966 

967 if self.submission.is_draft: 

968 step_id = "authors" 

969 stepper = get_submission_stepper(self.submission) 

970 stepper.set_active_step(step_id) 

971 context["stepper"] = stepper 

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

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

974 # button = Button( 

975 # id="next", 

976 # title=_("Next"), 

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

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

979 # ) 

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

981 # 

982 # form.buttons.append( 

983 # Button( 

984 # id="form_next", 

985 # title=_("Next"), 

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

987 # attrs={ 

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

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

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

991 # }, 

992 # ) 

993 # ) 

994 

995 context["form"] = form 

996 

997 # # Generate breadcrumb data 

998 # breadcrumb = get_submission_breadcrumb(self.submission) 

999 # breadcrumb.add_item( 

1000 # title=_("Authors"), 

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

1002 # ) 

1003 # context["breadcrumb"] = breadcrumb 

1004 

1005 return context 

1006 

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

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

1009 return self.remove_author(request) 

1010 elif self._FORM_ACTION_CORRESPONDING in request.POST: 

1011 return self.toggle_primary_author(request) 

1012 

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

1014 

1015 def add_author(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: 

1016 """ 

1017 Add an `SubmissionAuthor` to the submission. 

1018 """ 

1019 response = HttpResponseRedirect( 

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

1021 ) 

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

1023 if not form.is_valid(): 

1024 self.init_form = form 

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

1026 

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

1028 form.instance.submission = self.submission 

1029 form.save() 

1030 

1031 SubmissionLog.add_message( 

1032 self.submission, 

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

1034 content_en=_("Author added") 

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

1036 user=request.user, 

1037 significant=True, 

1038 ) 

1039 messages.success( 

1040 request, 

1041 _("Author added") 

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

1043 ) 

1044 

1045 return response 

1046 

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

1048 """ 

1049 Remove a `SubmissionAuthor` from the submission. 

1050 """ 

1051 response = HttpResponseRedirect( 

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

1053 ) 

1054 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors) 

1055 if not form.is_valid(): 

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

1057 return response 

1058 

1059 if len(self.authors) < 2: 

1060 messages.error( 

1061 request, 

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

1063 ) 

1064 return response 

1065 

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

1067 author_string = str(author) 

1068 author.delete() 

1069 

1070 SubmissionLog.add_message( 

1071 self.submission, 

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

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

1074 request=request.user, 

1075 significant=True, 

1076 ) 

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

1078 return response 

1079 

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

1081 """ 

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

1083 """ 

1084 response = HttpResponseRedirect( 

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

1086 ) 

1087 form = HiddenModelChoiceForm(request.POST, _queryset=self.authors) 

1088 if not form.is_valid(): 

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

1090 return response 

1091 

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

1093 author.corresponding = not author.corresponding 

1094 author.save() 

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

1096 messages.success( 

1097 request, 

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

1099 ) 

1100 

1101 return response 

1102 

1103 

1104class SubmissionConfirmView(RoleMixin, FormView): 

1105 """ 

1106 View to confirm the submission the current SubmissionVersion. 

1107 

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

1109 submitting a revised SubmissionVersion. 

1110 """ 

1111 

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

1113 submission: Submission 

1114 form_class = SubmissionConfirmForm 

1115 

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

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

1118 

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

1120 return True 

1121 

1122 return not self.submission.is_submittable 

1123 

1124 def get_success_url(self): 

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

1126 

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

1128 return self.submission.current_version # type:ignore 

1129 

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

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

1132 

1133 context["form"].buttons = [ 

1134 Button( 

1135 id="form_save", 

1136 title=_("Submit"), 

1137 icon_class="fa-check", 

1138 attrs={ 

1139 "type": ["submit"], 

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

1141 }, 

1142 ) 

1143 ] 

1144 

1145 if self.submission.is_draft: 

1146 stepper = get_submission_stepper(self.submission) 

1147 stepper.set_active_step("confirm") 

1148 context["stepper"] = stepper 

1149 

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

1151 

1152 files = [] 

1153 files.append( 

1154 { 

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

1156 "type": "Main", 

1157 } 

1158 ) 

1159 

1160 for file_wrapper in self.submission.current_version.additional_files.all(): # type:ignore 

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

1162 context["submission_files"] = files 

1163 

1164 # # Breadcrumb 

1165 # breadcrumb = get_submission_breadcrumb(self.submission) 

1166 # breadcrumb.add_item( 

1167 # title=_("Authors"), 

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

1169 # ) 

1170 # context["breadcrumb"] = breadcrumb 

1171 

1172 return context 

1173 

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

1175 """ 

1176 Submit the submission's current version. 

1177 """ 

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

1179 

1180 if self.submission.current_version.number == 1: # type:ignore 

1181 messages.success( 

1182 self.request, 

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

1184 ) 

1185 else: 

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

1187 

1188 previous_round_number = self.submission.current_version.number - 1 

1189 if previous_round_number > 0: 

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

1191 previous_reviewers = [ 

1192 review.reviewer 

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

1194 ] 

1195 for previous_reviewer in previous_reviewers: 

1196 qs = Suggestion.objects.filter( 

1197 submission=self.submission, suggested_user=previous_reviewer 

1198 ) 

1199 if not qs.exists(): 

1200 add_suggestion( 

1201 submission=self.submission, 

1202 suggested_user=previous_reviewer, 

1203 suggested_reviewer=None, 

1204 ) 

1205 

1206 return HttpResponseRedirect(self.get_success_url()) 

1207 

1208 

1209class SubmissionDeleteView(RoleMixin, DeleteView): 

1210 model: type[Submission] = Submission 

1211 submission: Submission 

1212 form_class = SubmissionDeleteForm 

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

1214 restricted_roles = [JournalManager, Author] 

1215 message_on_restrict = False 

1216 

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

1218 pk = kwargs["pk"] 

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

1220 

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

1222 

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

1224 """ 

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

1226 we already have the object loaded. 

1227 """ 

1228 return self.submission 

1229 

1230 def get_success_url(self): 

1231 return reverse_lazy("mesh:submission_list") 

1232 

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

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

1235 

1236 context["submission"] = self.submission 

1237 context["form"].buttons = [ 

1238 Button( 

1239 id="form_save", 

1240 title=_("Delete"), 

1241 icon_class="fa-trash", 

1242 attrs={ 

1243 "type": ["submit"], 

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

1245 }, 

1246 ) 

1247 ] 

1248 

1249 return context