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

373 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-27 14:14 +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 = f"{_('Response due date')} : <strong>{self.review.date_response_due.strftime('%d/%m/%Y')}</strong> <br>" 

270 description_2 += f"{_('Review due date')} : <strong>{self.review.date_review_due.strftime('%d/%m/%Y')}</strong>" 

271 description_3 = "Please fill the below form." 

272 context["form_description"] = [description_1, description_2, description_3] 

273 

274 return context 

275 

276 def get_success_url(self) -> str: 

277 return reverse( 

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

279 ) 

280 

281 

282class ReviewAcceptView(ReviewAnswerBaseView): 

283 """ 

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

285 """ 

286 

287 form_class = ReviewAcceptForm 

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

289 

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

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

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

293 

294 submission = self.review.version.submission 

295 # Generate breadcrumb data 

296 breadcrumb = get_submission_breadcrumb(submission) 

297 breadcrumb.add_item( 

298 title=_("Accept review"), 

299 url=reverse( 

300 "mesh:review_accept", 

301 kwargs={ 

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

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

304 "review_pk": self.review.pk, 

305 }, 

306 ), 

307 ) 

308 context["breadcrumb"] = breadcrumb 

309 

310 return context 

311 

312 def get_success_url(self) -> str: 

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

314 return reverse( 

315 "mesh:review_update", 

316 kwargs={ 

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

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

319 "review_pk": self.review.pk, 

320 }, 

321 ) 

322 

323 return super().get_success_url() 

324 

325 def form_valid(self, form): 

326 accept_comment = form.cleaned_data["accept_comment"] 

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

328 

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

330 

331 return HttpResponseRedirect(self.get_success_url()) 

332 

333 

334class ReviewDeclineView(ReviewAnswerBaseView): 

335 """ 

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

337 """ 

338 

339 form_class = ReviewDeclineForm 

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

341 

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

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

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

345 

346 submission = self.review.version.submission 

347 # Generate breadcrumb data 

348 breadcrumb = get_submission_breadcrumb(submission) 

349 breadcrumb.add_item( 

350 title=_("Decline review"), 

351 url=reverse( 

352 "mesh:review_decline", 

353 kwargs={ 

354 "submission_pk": submission.pk, 

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

356 "review_pk": self.review.pk, 

357 }, 

358 ), 

359 ) 

360 context["breadcrumb"] = breadcrumb 

361 

362 return context 

363 

364 def form_valid(self, form): 

365 person_info = { 

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

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

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

369 } 

370 submission = self.review.version.submission 

371 add_suggestion_from_person(submission, person_info, suggest_to_avoid=False) 

372 

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

374 

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

376 

377 return HttpResponseRedirect(self.get_success_url()) 

378 

379 

380class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView): 

381 """ 

382 View for performing a review on a submission version. 

383 """ 

384 

385 form_class = ReviewSubmitForm 

386 add_confirm_message = False 

387 

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

389 """ 

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

391 """ 

392 if self.review.state in [ 

393 ReviewState.AWAITING_ACCEPTANCE.value, 

394 ReviewState.DECLINED.value, 

395 ]: 

396 # Forward all query parameters 

397 return HttpResponseRedirect( 

398 format_url_with_params( 

399 reverse( 

400 "mesh:review_accept", 

401 kwargs={ 

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

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

404 "review_pk": self.review.pk, 

405 }, 

406 ), 

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

408 ) 

409 ) 

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

411 

412 def get_success_url(self) -> str: 

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

414 return self.submit_url() 

415 return reverse( 

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

417 ) 

418 

419 def submit_url(self) -> str: 

420 return reverse( 

421 "mesh:review_confirm", 

422 kwargs={ 

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

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

425 "review_pk": self.review.pk, 

426 }, 

427 ) 

428 

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

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

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

432 

433 submission = self.review.version.submission 

434 submission_url = reverse( 

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

436 ) 

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

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

439 description += "of submission " 

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

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

442 description += f"<br> By default, only the submission's editor, here {self.review.created_by.first_name} {self.review.created_by.last_name} will see your recommendation, review file(s) and comment." 

443 context["form_description"] = [description] 

444 

445 # Generate breadcrumb data 

446 breadcrumb = get_submission_breadcrumb(submission) 

447 breadcrumb.add_item( 

448 title=_("Submit review"), 

449 url=reverse( 

450 "mesh:review_update", 

451 kwargs={ 

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

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

454 "review_pk": self.review.pk, 

455 }, 

456 ), 

457 ) 

458 context["breadcrumb"] = breadcrumb 

459 

460 return context 

461 

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

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

464 

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

466 """ 

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

468 """ 

469 deletion_requested, _ = post_delete_model_file( 

470 self.review, request, self.request.current_role 

471 ) 

472 

473 if deletion_requested: 

474 self.review = self.get_review() 

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

476 

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

478 

479 

480class ReviewConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView): 

481 """ 

482 View to confirm the submission of a review. 

483 """ 

484 

485 review: Review 

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

487 form_class = ReviewConfirmForm 

488 

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

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

491 submission = self.get_submission() 

492 self.version = self.get_version() 

493 self.review = self.get_review() 

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

495 raise PermissionError 

496 

497 def get_success_url(self): 

498 return reverse( 

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

500 ) 

501 

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

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

504 

505 context["form"].buttons = [ 

506 Button( 

507 id="form_save", 

508 title=_("Submit"), 

509 icon_class="fa-check", 

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

511 ) 

512 ] 

513 

514 context["review_proxy"] = ReviewProxy( 

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

516 ) 

517 

518 files = [] 

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

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

521 context["review_files"] = files 

522 

523 # Breadcrumb 

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

525 breadcrumb.add_item( 

526 title=_("Submit review"), 

527 url=reverse( 

528 "mesh:review_update", 

529 kwargs={ 

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

531 "version_pk": self.version.pk, 

532 "review_pk": self.review.pk, 

533 }, 

534 ), 

535 ) 

536 context["breadcrumb"] = breadcrumb 

537 

538 return context 

539 

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

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

542 # self.review.submit() 

543 

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

545 

546 return HttpResponseRedirect(self.get_success_url()) 

547 

548 

549REVIEW_FILE_INPUT_PREFIX = "review_file_" 

550REVIEW_FILE_INPUT_VALUE = "true" 

551 

552 

553class ReviewDetails(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

554 """ 

555 Details view for a review. 

556 """ 

557 

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

559 review: Review 

560 

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

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

563 self.review = self.get_review() 

564 self.version = self.get_version() 

565 self.submission = self.get_submission() 

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

567 raise PermissionError 

568 

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

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

571 context["review_proxy"] = ReviewProxy( 

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

573 ) 

574 context["input_prefix"] = REVIEW_FILE_INPUT_PREFIX 

575 context["input_value"] = REVIEW_FILE_INPUT_VALUE 

576 

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

578 breadcrumb.add_item( 

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

580 url=reverse( 

581 "mesh:review_details", 

582 kwargs={ 

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

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

585 "review_pk": self.review.pk, 

586 }, 

587 ), 

588 ) 

589 context["breadcrumb"] = breadcrumb 

590 return context 

591 

592 

593class ReviewFileAccessUpdate(LoginRequiredMixin, MeshObjectMixin, View): 

594 """ 

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

596 """ 

597 

598 form_cache: str 

599 

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

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

602 review = self.get_review() 

603 

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

605 raise PermissionError 

606 

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

608 submission = self.get_submission() 

609 version = self.get_version() 

610 review = self.get_review() 

611 response = HttpResponseRedirect( 

612 reverse( 

613 "mesh:review_details", 

614 kwargs={ 

615 "submission_pk": submission.pk, 

616 "version_pk": version.pk, 

617 "review_pk": review.pk, 

618 }, 

619 ) 

620 ) 

621 files_qs = review.additional_files.all() 

622 if not files_qs: 

623 return response 

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

625 

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

627 # TODO NathanTY : wtf 

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

629 files_handled: list[int] = [] 

630 files_changed: list[ReviewAdditionalFile] = [] 

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

632 field_match = field_regex.match(field) 

633 if not field_match: 

634 continue 

635 if values[0] != REVIEW_FILE_INPUT_VALUE: 

636 continue 

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

638 file = files.get(file_pk) 

639 if file is None: 

640 continue 

641 # The boolean already has the correct value 

642 if file.author_access: 

643 files_handled.append(file_pk) 

644 continue 

645 file.author_access = True 

646 file.save() 

647 files_handled.append(file_pk) 

648 files_changed.append(file) 

649 

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

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

652 if file_pk not in files_handled: 

653 if not file.author_access: 

654 continue 

655 file.author_access = False 

656 file.save() 

657 files_changed.append(file) 

658 

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

660 

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

662 if changed_files_str: 

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

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

665 SubmissionLog.add_message( 

666 version.submission, 

667 content=message, 

668 content_en=message, 

669 user=request.user, 

670 significant=True, 

671 ) 

672 

673 return response 

674 

675 

676class ReviewAutoCreateView(LoginRequiredMixin, MeshObjectMixin, CreateView): 

677 """ 

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

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

680 """ 

681 

682 model = Review 

683 form_class = ReviewAutoCreateForm 

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

685 version: SubmissionVersion 

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

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

688 restricted_roles = [JournalManager, Editor] 

689 

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

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

692 self.version = self.get_version() 

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

694 raise PermissionError 

695 

696 # Reviewers already assigned to this round 

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

698 

699 submission = self.version.submission 

700 previous_round_number = self.version.number - 1 

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

702 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

703 state=ReviewState.SUBMITTED.value 

704 ).exclude( 

705 recommendation__in=[ 

706 RecommendationValue.REJECTED.value, 

707 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

708 ] 

709 ) 

710 

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

712 # not already assigned to this round. 

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

714 self.previous_reviewers = [ 

715 review.reviewer 

716 for review in reviews_that_can_be_auto_reassigned 

717 if review.reviewer not in self.existing_reviewers 

718 ] 

719 

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

721 return { 

722 "request_email": get_review_request_email( 

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

724 ), 

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

726 } 

727 

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

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

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

731 

732 submission = self.version.submission 

733 submission_url = reverse( 

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

735 ) 

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

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

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

739 

740 context["submission"] = submission 

741 

742 # Reviewers already assigned to this round 

743 context["existing_reviewers"] = self.existing_reviewers 

744 

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

746 # not already assigned to this round. 

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

748 context["previous_reviewers"] = self.previous_reviewers 

749 

750 context["form_description"] = [description] 

751 

752 # Generate breadcrumb data 

753 breadcrumb = get_submission_breadcrumb(submission) 

754 breadcrumb.add_item( 

755 title=_("Request review"), 

756 url=reverse( 

757 "mesh:review_create", 

758 kwargs={ 

759 "submission_pk": submission.pk, 

760 "version_pk": self.version.pk, 

761 }, 

762 ), 

763 ) 

764 context["breadcrumb"] = breadcrumb 

765 

766 return context 

767 

768 def form_valid(self, form): 

769 """ 

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

771 """ 

772 form.instance.version = self.version 

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

774 

775 if len(self.previous_reviewers) > 0: 

776 today = datetime.date.today() 

777 

778 reviewer = self.previous_reviewers[0] 

779 form.instance.reviewer = reviewer 

780 form.instance.date_response_due = today 

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

782 response = super().form_valid(form) 

783 

784 review = self.object 

785 send_review_request_email( 

786 review, 

787 form.cleaned_data["request_email"], 

788 form.cleaned_data["request_email_subject"], 

789 ) 

790 

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

792 review = Review( 

793 version=self.version, 

794 reviewer=reviewer, 

795 date_response_due=today, 

796 date_review_due=form.instance.date_review_due, 

797 state=ReviewState.PENDING.value, 

798 request_email=form.instance.request_email, 

799 ) 

800 review._user = self.request.user 

801 review.save() 

802 

803 send_review_request_email( 

804 review, 

805 form.cleaned_data["request_email"], 

806 form.cleaned_data["request_email_subject"], 

807 ) 

808 

809 messages.success( 

810 self.request, 

811 _("Referee request successfully submitted"), 

812 ) 

813 

814 # SubmissionLog.add_message( 

815 # self.version.submission, 

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

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

818 # user=self.request.user, 

819 # significant=True, 

820 # ) 

821 

822 else: 

823 response = super().form_invalid(form) 

824 

825 return response 

826 

827 def get_success_url(self) -> str: 

828 return reverse( 

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

830 )