Coverage for src / mesh / views / views_review.py: 67%

369 statements  

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

1import datetime 

2import re 

3from typing import Any 

4 

5from django.contrib import messages 

6from django.contrib.auth.mixins import LoginRequiredMixin 

7from django.http import HttpRequest, HttpResponse, HttpResponseRedirect 

8from django.urls import reverse 

9from django.utils.translation import gettext_lazy as _ 

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

11from django.views.generic.edit import CreateView, UpdateView 

12from ptf.url_utils import format_url_with_params 

13 

14from mesh.models.crud import send_review_request 

15from mesh.models.file_helpers import post_delete_model_file 

16from mesh.models.orm.review_models import ( 

17 RecommendationValue, 

18 Review, 

19 ReviewAdditionalFile, 

20 ReviewState, 

21) 

22from mesh.models.orm.submission_models import SubmissionLog, SubmissionVersion 

23from mesh.models.orm.suggestion_model import Suggestion 

24from mesh.models.orm.user_models import User 

25from mesh.models.roles.editor import Editor 

26from mesh.models.roles.journal_manager import JournalManager 

27from mesh.models.roles.reviewer import Reviewer 

28from mesh.views.components.breadcrumb import get_submission_breadcrumb 

29from mesh.views.components.button import Button 

30from mesh.views.forms.base_forms import FormAction 

31from mesh.views.forms.review_forms import ( 

32 ReviewAcceptForm, 

33 ReviewAutoCreateForm, 

34 ReviewConfirmForm, 

35 ReviewCreateForm, 

36 ReviewDeclineForm, 

37 ReviewSubmitForm, 

38) 

39from mesh.views.utils import create_new_user, get_review_request_email, send_review_request_email 

40from mesh.views.viewmodel import ReviewProxy 

41from mesh.views.views_base import MeshObjectMixin, SubmittableModelFormMixin 

42from mesh.views.views_reviewer import add_suggestion_from_person 

43 

44 

45class ReviewCreateView(LoginRequiredMixin, MeshObjectMixin, FormView): 

46 """ 

47 View to invite a new reviewer for a given round. 

48 The review form is filled with the submission version from the URL. 

49 """ 

50 

51 model = Review 

52 form_class = ReviewCreateForm 

53 template_name = "mesh/review/review_create.html" 

54 # template_name = "mesh/forms/form_full_page.html" 

55 version: SubmissionVersion 

56 

57 # restricted_roles = [JournalManager, Editor] 

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

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

60 self.version = self.get_version() 

61 if not self.request.current_role.can_invite_reviewer(self.version): 

62 raise PermissionError 

63 

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

65 kwargs = super().get_form_kwargs() 

66 suggestions = Suggestion.objects.filter(submission=self.version.submission).order_by("seq") 

67 choices = [(suggestion.pk, "") for suggestion in suggestions] 

68 kwargs["_choices"] = choices 

69 kwargs["_version"] = self.version 

70 # # submission = self.version.submission 

71 # # self.suggestions = Suggestion.objects.filter(submission=submission) 

72 # # self.existing_reviewers = [review.reviewer for review in self.version.reviews.all()] 

73 # # kwargs["_suggestions"] = self.suggestions 

74 # # kwargs["_existing_reviewers"] = self.existing_reviewers 

75 # 

76 # # current_reviewers = list( 

77 # # Review.objects.filter(version=self.version).values_list("reviewer__pk", flat=True) 

78 # # ) 

79 # # kwargs["_reviewers"] = User.objects.exclude(pk__in=current_reviewers) 

80 

81 return kwargs 

82 

83 def get_initial(self) -> dict[str, Any]: 

84 return { 

85 "request_email": get_review_request_email( 

86 self.version.submission, self.request.build_absolute_uri("/") 

87 ), 

88 "request_email_subject": _("New referee request"), 

89 "quick_request_email": get_review_request_email( 

90 self.version.submission, 

91 self.request.build_absolute_uri("/"), 

92 "mesh/emails/review/referee_quick_request.html", 

93 ), 

94 "quick_request_email_subject": _("New referee request for a quick review"), 

95 } 

96 

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

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

99 context["page_title"] = "Request review" 

100 context["version"] = self.version 

101 

102 submission = self.version.submission 

103 submission_url = reverse( 

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

105 ) 

106 description = "Please fill the form below to request an additional review for " 

107 description += f"round #{self.version.number} of submission " 

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

109 

110 shortlist_url = reverse( 

111 "mesh:submission_shortlist", kwargs={"submission_pk": submission.pk} 

112 ) 

113 action = f"<a href='{shortlist_url}'><button>Edit Shortlist</button></a>" 

114 context["submission"] = submission 

115 

116 self.suggestions = Suggestion.objects.filter(submission=submission).order_by("seq") 

117 context["suggestions"] = self.suggestions 

118 

119 self.existing_reviewers = [review.reviewer for review in self.version.reviews_censored] 

120 context["existing_reviewers"] = self.existing_reviewers 

121 

122 choices = [] 

123 has_selected_choice = False 

124 for suggestion in self.suggestions: 

125 suggested_user = ( 

126 suggestion.suggested_user 

127 if suggestion.suggested_user is not None 

128 else suggestion.suggested_reviewer 

129 ) 

130 if suggested_user in self.existing_reviewers: 

131 choices.append( 

132 { 

133 "value": suggestion.pk, 

134 "label": str(suggested_user), 

135 "disabled": True, 

136 "avoid": suggestion.suggest_to_avoid, 

137 } 

138 ) 

139 else: 

140 choice = { 

141 "value": suggestion.pk, 

142 "label": str(suggested_user), 

143 "avoid": suggestion.suggest_to_avoid, 

144 } 

145 if not has_selected_choice: 

146 has_selected_choice = True 

147 choice["selected"] = True 

148 choices.append(choice) 

149 context["suggested_users"] = choices 

150 

151 context["form_description"] = [description, action] 

152 

153 # Generate breadcrumb data 

154 breadcrumb = get_submission_breadcrumb(submission) 

155 breadcrumb.add_item( 

156 title=_("Request review"), 

157 url=reverse( 

158 "mesh:review_create", 

159 kwargs={ 

160 "submission_pk": self.version.submission.pk, 

161 "version_pk": self.version.pk, 

162 }, 

163 ), 

164 ) 

165 context["breadcrumb"] = breadcrumb 

166 

167 return context 

168 

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

170 # Check if the review is attached to an existing user or if we should create a new one. 

171 reviewer = None 

172 suggestion = None 

173 old_suggested_user = None 

174 email = first_name = last_name = "" 

175 

176 reviewer_select = form.cleaned_data["reviewer_select"] 

177 if reviewer_select == "shortlist": 

178 # someone from the shortlist was selected. 

179 # Get the associated suggestion 

180 suggestion_pk = form.cleaned_data["suggested_user"] 

181 suggestion = Suggestion.objects.get(pk=int(suggestion_pk)) 

182 

183 # A suggestion was made either on an existing user or with first name/last name/email 

184 if suggestion.suggested_user is not None: 

185 # An existing User was selected. No need to create a user 

186 reviewer = suggestion.suggested_user 

187 else: 

188 old_suggested_user = suggestion.suggested_reviewer 

189 first_name = suggestion.suggested_reviewer.first_name 

190 last_name = suggestion.suggested_reviewer.last_name 

191 email = suggestion.suggested_reviewer.email 

192 else: 

193 # First name, last name and email were typed in the form 

194 first_name = form.cleaned_data["reviewer_first_name"] 

195 last_name = form.cleaned_data["reviewer_last_name"] 

196 email = form.cleaned_data["reviewer_email"] 

197 

198 if reviewer is None: 

199 # Even if first/last/email were typed (some time ago in the case of a suggestion) 

200 # There might now exists a User with the same email 

201 reviewer = User.objects.filter(email=email).first() 

202 if reviewer is None: 

203 reviewer = create_new_user(email, first_name, last_name) 

204 Suggestion.objects.filter(suggested_reviewer__email=email).update( 

205 suggested_reviewer=None, suggested_user=reviewer 

206 ) 

207 

208 if old_suggested_user is not None: 

209 # reviewer is now a User. We need to merge data from the suggested_reviewer 

210 reviewer.keywords = old_suggested_user.keywords 

211 reviewer.save() 

212 

213 old_suggested_user.delete() 

214 

215 # We now know the reviewer. Set the form.instance so that Django can create a Review 

216 

217 review = send_review_request(self.request, reviewer, self.version, form) 

218 # review = send_review_request(self.request.user, reviewer, self.version, form) 

219 

220 messages.success( 

221 self.request, 

222 _(f"Referee request successfully submitted to {review.reviewer}"), 

223 ) 

224 # Redirect 

225 return super().form_valid(form) 

226 

227 def get_success_url(self) -> str: 

228 return reverse( 

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

230 ) 

231 

232 

233class ReviewEditBaseView(LoginRequiredMixin, MeshObjectMixin, UpdateView): 

234 """ 

235 Base view for a reviewer to edit a review. 

236 """ 

237 

238 model = Review 

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

240 review: Review 

241 restricted_roles = [Reviewer] 

242 

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

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

245 self.review = self.get_review() 

246 if not self.request.current_role.can_edit_review(self.get_submission(), self.review): 

247 raise PermissionError 

248 

249 def get_object(self, *args, **kwargs): 

250 return self.review 

251 

252 def get_initial(self) -> dict[str, Any]: 

253 return { 

254 "date_response_due_display": self.review.date_response_due, 

255 "date_review_due_display": self.review.date_review_due, 

256 } 

257 

258 

259class ReviewAnswerBaseView(ReviewEditBaseView): 

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

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

262 submission = self.review.version.submission 

263 submission_url = reverse( 

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

265 ) 

266 # TODO: Create a custom template instead of passing HTML in variable ? 

267 description_1 = f"{self.review.created_by} requested your review for the submission " 

268 description_1 += f"<a href='{submission_url}'><i>{submission.name}</i></a>." 

269 description_2 = "Please fill the below form." 

270 context["form_description"] = [description_1, description_2] 

271 

272 return context 

273 

274 def get_success_url(self) -> str: 

275 return reverse( 

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

277 ) 

278 

279 

280class ReviewAcceptView(ReviewAnswerBaseView): 

281 """ 

282 Preliminary view for a reviewer to accept a requested review. 

283 """ 

284 

285 form_class = ReviewAcceptForm 

286 template_name = "mesh/review/review_accept.html" 

287 

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

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

290 context["page_title"] = "Accept review request" 

291 

292 submission = self.review.version.submission 

293 # Generate breadcrumb data 

294 breadcrumb = get_submission_breadcrumb(submission) 

295 breadcrumb.add_item( 

296 title=_("Accept review"), 

297 url=reverse( 

298 "mesh:review_accept", 

299 kwargs={ 

300 "submission_pk": self.review.version.submission.pk, 

301 "version_pk": self.review.version.pk, 

302 "review_pk": self.review.pk, 

303 }, 

304 ), 

305 ) 

306 context["breadcrumb"] = breadcrumb 

307 

308 return context 

309 

310 def get_success_url(self) -> str: 

311 if self.review.state == ReviewState.PENDING.value: 

312 return reverse( 

313 "mesh:review_update", 

314 kwargs={ 

315 "submission_pk": self.review.version.submission.pk, 

316 "version_pk": self.review.version.pk, 

317 "review_pk": self.review.pk, 

318 }, 

319 ) 

320 

321 return super().get_success_url() 

322 

323 def form_valid(self, form): 

324 accept_comment = form.cleaned_data["accept_comment"] 

325 self.review.accept(True, accept_comment, user=self.request.user) 

326 

327 messages.success(self.request, _("Review request successfully accepted")) 

328 

329 return HttpResponseRedirect(self.get_success_url()) 

330 

331 

332class ReviewDeclineView(ReviewAnswerBaseView): 

333 """ 

334 Preliminary view for a reviewer to decline a requested review. 

335 """ 

336 

337 form_class = ReviewDeclineForm 

338 template_name = "mesh/review/review_decline.html" 

339 

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

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

342 context["page_title"] = "Decline review request" 

343 

344 submission = self.review.version.submission 

345 # Generate breadcrumb data 

346 breadcrumb = get_submission_breadcrumb(submission) 

347 breadcrumb.add_item( 

348 title=_("Decline review"), 

349 url=reverse( 

350 "mesh:review_decline", 

351 kwargs={ 

352 "submission_pk": submission.pk, 

353 "version_pk": self.review.version.pk, 

354 "review_pk": self.review.pk, 

355 }, 

356 ), 

357 ) 

358 context["breadcrumb"] = breadcrumb 

359 

360 return context 

361 

362 def form_valid(self, form): 

363 person_info = { 

364 "first_name": form.data.get("reviewer_first_name", ""), 

365 "last_name": form.data.get("reviewer_last_name", ""), 

366 "email": form.data.get("reviewer_email", ""), 

367 } 

368 submission = self.review.version.submission 

369 add_suggestion_from_person(submission, person_info, suggest_to_avoid=False) 

370 

371 self.review.accept(accept_value=False, accept_comment="", user=self.request.user) 

372 

373 messages.success(self.request, _("Review request successfully declined")) 

374 

375 return HttpResponseRedirect(self.get_success_url()) 

376 

377 

378class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView): 

379 """ 

380 View for performing a review on a submission version. 

381 """ 

382 

383 form_class = ReviewSubmitForm 

384 add_confirm_message = False 

385 

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

387 """ 

388 Redirect to the accept form if the review has not been accepted yet. 

389 """ 

390 if self.review.state in [ 

391 ReviewState.AWAITING_ACCEPTANCE.value, 

392 ReviewState.DECLINED.value, 

393 ]: 

394 # Forward all query parameters 

395 return HttpResponseRedirect( 

396 format_url_with_params( 

397 reverse( 

398 "mesh:review_accept", 

399 kwargs={ 

400 "submission_pk": self.review.version.submission.pk, 

401 "version_pk": self.review.version.pk, 

402 "review_pk": self.review.pk, 

403 }, 

404 ), 

405 {k: v for k, v in request.GET.lists()}, 

406 ) 

407 ) 

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

409 

410 def get_success_url(self) -> str: 

411 if FormAction.SUBMIT.value in self.request.POST: 

412 return self.submit_url() 

413 return reverse( 

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

415 ) 

416 

417 def submit_url(self) -> str: 

418 return reverse( 

419 "mesh:review_confirm", 

420 kwargs={ 

421 "submission_pk": self.review.version.submission.pk, 

422 "version_pk": self.review.version.pk, 

423 "review_pk": self.review.pk, 

424 }, 

425 ) 

426 

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

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

429 context["page_title"] = "Submit review" 

430 

431 submission = self.review.version.submission 

432 submission_url = reverse( 

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

434 ) 

435 # TODO: Create a custom template instead of passing HTML in variable 

436 description = f"Please submit your review for round #{self.review.version.number} " 

437 description += "of submission " 

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

439 description += "by filling the below form." 

440 context["form_description"] = [description] 

441 

442 # Generate breadcrumb data 

443 breadcrumb = get_submission_breadcrumb(submission) 

444 breadcrumb.add_item( 

445 title=_("Submit review"), 

446 url=reverse( 

447 "mesh:review_update", 

448 kwargs={ 

449 "submission_pk": self.review.version.submission.pk, 

450 "version_pk": self.review.version.pk, 

451 "review_pk": self.review.pk, 

452 }, 

453 ), 

454 ) 

455 context["breadcrumb"] = breadcrumb 

456 

457 return context 

458 

459 def form_pre_save(self, form) -> None: 

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

461 

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

463 """ 

464 View for updating the review or deleting one of the associated files. 

465 """ 

466 deletion_requested, _ = post_delete_model_file( 

467 self.review, request, self.request.current_role 

468 ) 

469 

470 if deletion_requested: 

471 self.review = self.get_review() 

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

473 

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

475 

476 

477class ReviewConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView): 

478 """ 

479 View to confirm the submission of a review. 

480 """ 

481 

482 review: Review 

483 template_name = "mesh/review/review_confirm.html" 

484 form_class = ReviewConfirmForm 

485 

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

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

488 submission = self.get_submission() 

489 self.review = self.get_review() 

490 if not self.request.current_role.can_submit_review(submission, self.review): 

491 raise PermissionError 

492 

493 def get_success_url(self): 

494 return reverse( 

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

496 ) 

497 

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

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

500 

501 context["form"].buttons = [ 

502 Button( 

503 id="form_save", 

504 title=_("Submit"), 

505 icon_class="fa-check", 

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

507 ) 

508 ] 

509 

510 context["review_proxy"] = ReviewProxy(self.review, self.request.current_role) 

511 

512 files = [] 

513 for file_wrapper in self.review.additional_files.all(): 

514 files.append({"file_wrapper": file_wrapper, "type": "Review file"}) 

515 context["review_files"] = files 

516 

517 # Breadcrumb 

518 breadcrumb = get_submission_breadcrumb(self.review.version.submission) 

519 breadcrumb.add_item( 

520 title=_("Submit review"), 

521 url=reverse("mesh:review_update", kwargs={"pk": self.review.pk}), 

522 ) 

523 context["breadcrumb"] = breadcrumb 

524 

525 return context 

526 

527 def form_valid(self, form: ReviewConfirmForm) -> HttpResponse: 

528 self.review.submit(self.request.user) 

529 # self.review.submit() 

530 

531 messages.success(self.request, _("Your review has been successfully submitted.")) 

532 

533 return HttpResponseRedirect(self.get_success_url()) 

534 

535 

536REVIEW_FILE_INPUT_PREFIX = "review_file_" 

537REVIEW_FILE_INPUT_VALUE = "true" 

538 

539 

540class ReviewDetails(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

541 """ 

542 Details view for a review. 

543 """ 

544 

545 template_name = "mesh/review/review_details.html" 

546 review: Review 

547 

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

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

550 self.review = self.get_review() 

551 self.version = self.get_version() 

552 self.submission = self.get_submission() 

553 if not self.request.current_role.can_access_review(self.submission, self.review): 

554 raise PermissionError 

555 

556 def get_context_data(self, *args, **kwargs) -> dict[str, Any]: 

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

558 context["review_proxy"] = ReviewProxy( 

559 self.review, self.request.current_role, version=self.version 

560 ) 

561 context["input_prefix"] = REVIEW_FILE_INPUT_PREFIX 

562 context["input_value"] = REVIEW_FILE_INPUT_VALUE 

563 

564 breadcrumb = get_submission_breadcrumb(self.review.version.submission) 

565 breadcrumb.add_item( 

566 title=_("Review details") + f" - {self.review.reviewer}", 

567 url=reverse( 

568 "mesh:review_details", 

569 kwargs={ 

570 "submission_pk": self.review.version.submission.pk, 

571 "version_pk": self.review.version.pk, 

572 "review_pk": self.review.pk, 

573 }, 

574 ), 

575 ) 

576 context["breadcrumb"] = breadcrumb 

577 return context 

578 

579 

580class ReviewFileAccessUpdate(LoginRequiredMixin, MeshObjectMixin, View): 

581 """ 

582 View to edit the author access to a review's files. 

583 """ 

584 

585 form_cache: str 

586 

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

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

589 review = self.get_review() 

590 

591 if not self.request.current_role.can_edit_review_file_right(review): 

592 raise PermissionError 

593 

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

595 submission = self.get_submission() 

596 version = self.get_version() 

597 review = self.get_review() 

598 response = HttpResponseRedirect( 

599 reverse( 

600 "mesh:review_details", 

601 kwargs={ 

602 "submission_pk": submission.pk, 

603 "version_pk": version.pk, 

604 "review_pk": review.pk, 

605 }, 

606 ) 

607 ) 

608 files_qs = review.additional_files.all() 

609 if not files_qs: 

610 return response 

611 files = {f.pk: f for f in files_qs} 

612 

613 # Check the POST data and look for the file inputs. Update data accordingly 

614 # TODO NathanTY : wtf 

615 field_regex = re.compile(f"{REVIEW_FILE_INPUT_PREFIX}(?P<pk>[0-9]+)$") 

616 files_handled: list[int] = [] 

617 files_changed: list[ReviewAdditionalFile] = [] 

618 for field, values in request.POST.lists(): 

619 field_match = field_regex.match(field) 

620 if not field_match: 

621 continue 

622 if values[0] != REVIEW_FILE_INPUT_VALUE: 

623 continue 

624 file_pk = int(field_match.group("pk")) 

625 file = files.get(file_pk) 

626 if file is None: 

627 continue 

628 # The boolean already has the correct value 

629 if file.author_access: 

630 files_handled.append(file_pk) 

631 continue 

632 file.author_access = True 

633 file.save() 

634 files_handled.append(file_pk) 

635 files_changed.append(file) 

636 

637 # There is no input in the POST data for unchecked checkboxes 

638 for file_pk, file in files.items(): 

639 if file_pk not in files_handled: 

640 if not file.author_access: 

641 continue 

642 file.author_access = False 

643 file.save() 

644 files_changed.append(file) 

645 

646 messages.success(request, _("Successfully updated the author access right.")) 

647 

648 changed_files_str = ", ".join([f.name for f in files_changed]) 

649 if changed_files_str: 

650 message = f"Changed author access right to Round #{version.number}" 

651 message += f" files: {changed_files_str}" 

652 SubmissionLog.add_message( 

653 version.submission, 

654 content=message, 

655 content_en=message, 

656 user=request.user, 

657 significant=True, 

658 ) 

659 

660 return response 

661 

662 

663class ReviewAutoCreateView(LoginRequiredMixin, MeshObjectMixin, CreateView): 

664 """ 

665 View to invite a new reviewer for a given round. 

666 The review form is filled with the submission version from the URL. 

667 """ 

668 

669 model = Review 

670 form_class = ReviewAutoCreateForm 

671 template_name = "mesh/review/review_auto_create.html" 

672 version: SubmissionVersion 

673 previous_reviewers = [] # Reviewers of the previous round that will be assigned to the current round 

674 existing_reviewers = [] # Reviewers already assigned to this round 

675 restricted_roles = [JournalManager, Editor] 

676 

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

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

679 self.version = self.get_version() 

680 if not self.request.current_role.can_invite_reviewer(self.version): 

681 raise PermissionError 

682 

683 # Reviewers already assigned to this round 

684 self.existing_reviewers = [review.reviewer for review in self.version.reviews_censored] 

685 

686 submission = self.version.submission 

687 previous_round_number = self.version.number - 1 

688 previous_round = submission.versions.get(number=previous_round_number) 

689 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

690 state=ReviewState.SUBMITTED.value 

691 ).exclude( 

692 recommendation__in=[ 

693 RecommendationValue.REJECTED.value, 

694 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

695 ] 

696 ) 

697 

698 # Reviewers of the previous round that submitted a "positive" recommendation (Accept or Revision requested) 

699 # not already assigned to this round. 

700 # These reviewers will be assigned to the current round if the editor validates the form. 

701 self.previous_reviewers = [ 

702 review.reviewer 

703 for review in reviews_that_can_be_auto_reassigned 

704 if review.reviewer not in self.existing_reviewers 

705 ] 

706 

707 def get_initial(self) -> dict[str, Any]: 

708 return { 

709 "request_email": get_review_request_email( 

710 self.version.submission, self.request.build_absolute_uri("/") 

711 ), 

712 "request_email_subject": _("Referee request after revision"), 

713 } 

714 

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

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

717 context["page_title"] = "Request review" 

718 

719 submission = self.version.submission 

720 submission_url = reverse( 

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

722 ) 

723 description = "Please fill the form below to request additional reviews for " 

724 description += f"round #{self.version.number} of submission " 

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

726 

727 context["submission"] = submission 

728 

729 # Reviewers already assigned to this round 

730 context["existing_reviewers"] = self.existing_reviewers 

731 

732 # Reviewers of the previous round that submitted a "positive" recommendation (Accept or Revision requested) 

733 # not already assigned to this round. 

734 # These reviewers will be assigned to the current round if the editor validates the form. 

735 context["previous_reviewers"] = self.previous_reviewers 

736 

737 context["form_description"] = [description] 

738 

739 # Generate breadcrumb data 

740 breadcrumb = get_submission_breadcrumb(submission) 

741 breadcrumb.add_item( 

742 title=_("Request review"), 

743 url=reverse( 

744 "mesh:review_create", 

745 kwargs={ 

746 "submission_pk": submission.pk, 

747 "version_pk": self.version.pk, 

748 }, 

749 ), 

750 ) 

751 context["breadcrumb"] = breadcrumb 

752 

753 return context 

754 

755 def form_valid(self, form): 

756 """ 

757 Inject required data to the form instance before saving model. 

758 """ 

759 form.instance.version = self.version 

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

761 

762 if len(self.previous_reviewers) > 0: 

763 today = datetime.date.today() 

764 

765 reviewer = self.previous_reviewers[0] 

766 form.instance.reviewer = reviewer 

767 form.instance.date_response_due = today 

768 form.instance.state = ReviewState.PENDING.value 

769 response = super().form_valid(form) 

770 

771 review = self.object 

772 send_review_request_email( 

773 review, 

774 form.cleaned_data["request_email"], 

775 form.cleaned_data["request_email_subject"], 

776 self.request.build_absolute_uri("/"), 

777 ) 

778 

779 for reviewer in self.previous_reviewers[1:]: 

780 review = Review( 

781 version=self.version, 

782 reviewer=reviewer, 

783 date_response_due=today, 

784 date_review_due=form.instance.date_review_due, 

785 state=ReviewState.PENDING.value, 

786 request_email=form.instance.request_email, 

787 ) 

788 review._user = self.request.user 

789 review.save() 

790 

791 send_review_request_email( 

792 review, 

793 form.cleaned_data["request_email"], 

794 form.cleaned_data["request_email_subject"], 

795 self.request.build_absolute_uri("/"), 

796 ) 

797 

798 messages.success( 

799 self.request, 

800 _("Referee request successfully submitted"), 

801 ) 

802 

803 # SubmissionLog.add_message( 

804 # self.version.submission, 

805 # content=_("Referee request sent to") + f" {review.reviewer}", 

806 # content_en=f"Referee request sent to {review.reviewer}", 

807 # user=self.request.user, 

808 # significant=True, 

809 # ) 

810 

811 else: 

812 response = super().form_invalid(form) 

813 

814 return response 

815 

816 def get_success_url(self) -> str: 

817 return reverse( 

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

819 )