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

372 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-09 13: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 context["form_description"] = [description] 

443 

444 # Generate breadcrumb data 

445 breadcrumb = get_submission_breadcrumb(submission) 

446 breadcrumb.add_item( 

447 title=_("Submit review"), 

448 url=reverse( 

449 "mesh:review_update", 

450 kwargs={ 

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

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

453 "review_pk": self.review.pk, 

454 }, 

455 ), 

456 ) 

457 context["breadcrumb"] = breadcrumb 

458 

459 return context 

460 

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

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

463 

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

465 """ 

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

467 """ 

468 deletion_requested, _ = post_delete_model_file( 

469 self.review, request, self.request.current_role 

470 ) 

471 

472 if deletion_requested: 

473 self.review = self.get_review() 

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

475 

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

477 

478 

479class ReviewConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView): 

480 """ 

481 View to confirm the submission of a review. 

482 """ 

483 

484 review: Review 

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

486 form_class = ReviewConfirmForm 

487 

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

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

490 submission = self.get_submission() 

491 self.version = self.get_version() 

492 self.review = self.get_review() 

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

494 raise PermissionError 

495 

496 def get_success_url(self): 

497 return reverse( 

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

499 ) 

500 

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

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

503 

504 context["form"].buttons = [ 

505 Button( 

506 id="form_save", 

507 title=_("Submit"), 

508 icon_class="fa-check", 

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

510 ) 

511 ] 

512 

513 context["review_proxy"] = ReviewProxy( 

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

515 ) 

516 

517 files = [] 

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

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

520 context["review_files"] = files 

521 

522 # Breadcrumb 

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

524 breadcrumb.add_item( 

525 title=_("Submit review"), 

526 url=reverse( 

527 "mesh:review_update", 

528 kwargs={ 

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

530 "version_pk": self.version.pk, 

531 "review_pk": self.review.pk, 

532 }, 

533 ), 

534 ) 

535 context["breadcrumb"] = breadcrumb 

536 

537 return context 

538 

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

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

541 # self.review.submit() 

542 

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

544 

545 return HttpResponseRedirect(self.get_success_url()) 

546 

547 

548REVIEW_FILE_INPUT_PREFIX = "review_file_" 

549REVIEW_FILE_INPUT_VALUE = "true" 

550 

551 

552class ReviewDetails(LoginRequiredMixin, MeshObjectMixin, TemplateView): 

553 """ 

554 Details view for a review. 

555 """ 

556 

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

558 review: Review 

559 

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

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

562 self.review = self.get_review() 

563 self.version = self.get_version() 

564 self.submission = self.get_submission() 

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

566 raise PermissionError 

567 

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

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

570 context["review_proxy"] = ReviewProxy( 

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

572 ) 

573 context["input_prefix"] = REVIEW_FILE_INPUT_PREFIX 

574 context["input_value"] = REVIEW_FILE_INPUT_VALUE 

575 

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

577 breadcrumb.add_item( 

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

579 url=reverse( 

580 "mesh:review_details", 

581 kwargs={ 

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

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

584 "review_pk": self.review.pk, 

585 }, 

586 ), 

587 ) 

588 context["breadcrumb"] = breadcrumb 

589 return context 

590 

591 

592class ReviewFileAccessUpdate(LoginRequiredMixin, MeshObjectMixin, View): 

593 """ 

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

595 """ 

596 

597 form_cache: str 

598 

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

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

601 review = self.get_review() 

602 

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

604 raise PermissionError 

605 

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

607 submission = self.get_submission() 

608 version = self.get_version() 

609 review = self.get_review() 

610 response = HttpResponseRedirect( 

611 reverse( 

612 "mesh:review_details", 

613 kwargs={ 

614 "submission_pk": submission.pk, 

615 "version_pk": version.pk, 

616 "review_pk": review.pk, 

617 }, 

618 ) 

619 ) 

620 files_qs = review.additional_files.all() 

621 if not files_qs: 

622 return response 

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

624 

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

626 # TODO NathanTY : wtf 

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

628 files_handled: list[int] = [] 

629 files_changed: list[ReviewAdditionalFile] = [] 

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

631 field_match = field_regex.match(field) 

632 if not field_match: 

633 continue 

634 if values[0] != REVIEW_FILE_INPUT_VALUE: 

635 continue 

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

637 file = files.get(file_pk) 

638 if file is None: 

639 continue 

640 # The boolean already has the correct value 

641 if file.author_access: 

642 files_handled.append(file_pk) 

643 continue 

644 file.author_access = True 

645 file.save() 

646 files_handled.append(file_pk) 

647 files_changed.append(file) 

648 

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

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

651 if file_pk not in files_handled: 

652 if not file.author_access: 

653 continue 

654 file.author_access = False 

655 file.save() 

656 files_changed.append(file) 

657 

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

659 

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

661 if changed_files_str: 

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

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

664 SubmissionLog.add_message( 

665 version.submission, 

666 content=message, 

667 content_en=message, 

668 user=request.user, 

669 significant=True, 

670 ) 

671 

672 return response 

673 

674 

675class ReviewAutoCreateView(LoginRequiredMixin, MeshObjectMixin, CreateView): 

676 """ 

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

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

679 """ 

680 

681 model = Review 

682 form_class = ReviewAutoCreateForm 

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

684 version: SubmissionVersion 

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

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

687 restricted_roles = [JournalManager, Editor] 

688 

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

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

691 self.version = self.get_version() 

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

693 raise PermissionError 

694 

695 # Reviewers already assigned to this round 

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

697 

698 submission = self.version.submission 

699 previous_round_number = self.version.number - 1 

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

701 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

702 state=ReviewState.SUBMITTED.value 

703 ).exclude( 

704 recommendation__in=[ 

705 RecommendationValue.REJECTED.value, 

706 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

707 ] 

708 ) 

709 

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

711 # not already assigned to this round. 

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

713 self.previous_reviewers = [ 

714 review.reviewer 

715 for review in reviews_that_can_be_auto_reassigned 

716 if review.reviewer not in self.existing_reviewers 

717 ] 

718 

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

720 return { 

721 "request_email": get_review_request_email( 

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

723 ), 

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

725 } 

726 

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

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

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

730 

731 submission = self.version.submission 

732 submission_url = reverse( 

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

734 ) 

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

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

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

738 

739 context["submission"] = submission 

740 

741 # Reviewers already assigned to this round 

742 context["existing_reviewers"] = self.existing_reviewers 

743 

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

745 # not already assigned to this round. 

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

747 context["previous_reviewers"] = self.previous_reviewers 

748 

749 context["form_description"] = [description] 

750 

751 # Generate breadcrumb data 

752 breadcrumb = get_submission_breadcrumb(submission) 

753 breadcrumb.add_item( 

754 title=_("Request review"), 

755 url=reverse( 

756 "mesh:review_create", 

757 kwargs={ 

758 "submission_pk": submission.pk, 

759 "version_pk": self.version.pk, 

760 }, 

761 ), 

762 ) 

763 context["breadcrumb"] = breadcrumb 

764 

765 return context 

766 

767 def form_valid(self, form): 

768 """ 

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

770 """ 

771 form.instance.version = self.version 

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

773 

774 if len(self.previous_reviewers) > 0: 

775 today = datetime.date.today() 

776 

777 reviewer = self.previous_reviewers[0] 

778 form.instance.reviewer = reviewer 

779 form.instance.date_response_due = today 

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

781 response = super().form_valid(form) 

782 

783 review = self.object 

784 send_review_request_email( 

785 review, 

786 form.cleaned_data["request_email"], 

787 form.cleaned_data["request_email_subject"], 

788 ) 

789 

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

791 review = Review( 

792 version=self.version, 

793 reviewer=reviewer, 

794 date_response_due=today, 

795 date_review_due=form.instance.date_review_due, 

796 state=ReviewState.PENDING.value, 

797 request_email=form.instance.request_email, 

798 ) 

799 review._user = self.request.user 

800 review.save() 

801 

802 send_review_request_email( 

803 review, 

804 form.cleaned_data["request_email"], 

805 form.cleaned_data["request_email_subject"], 

806 ) 

807 

808 messages.success( 

809 self.request, 

810 _("Referee request successfully submitted"), 

811 ) 

812 

813 # SubmissionLog.add_message( 

814 # self.version.submission, 

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

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

817 # user=self.request.user, 

818 # significant=True, 

819 # ) 

820 

821 else: 

822 response = super().form_invalid(form) 

823 

824 return response 

825 

826 def get_success_url(self) -> str: 

827 return reverse( 

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

829 )