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

391 statements  

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

1import datetime 

2import re 

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, 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, UpdateView 

13from ptf.url_utils import format_url_with_params 

14 

15from mesh.model.file_helpers import post_delete_model_file 

16from mesh.model.roles.editor import Editor 

17from mesh.model.roles.journal_manager import JournalManager 

18from mesh.model.roles.reviewer import Reviewer 

19from mesh.views.forms.base_forms import FormAction 

20from mesh.views.forms.review_forms import ( 

21 ReviewAcceptForm, 

22 ReviewAutoCreateForm, 

23 ReviewConfirmForm, 

24 ReviewCreateForm, 

25 ReviewDeclineForm, 

26 ReviewSubmitForm, 

27) 

28from mesh.views.mixins import RoleMixin 

29 

30from ..models.review_models import RecommendationValue, Review, ReviewAdditionalFile, ReviewState 

31from ..models.submission_models import SubmissionLog, SubmissionVersion 

32from ..models.user_models import Suggestion, User 

33from ..views.views_reviewer import add_suggestion_from_person 

34from .components.breadcrumb import get_submission_breadcrumb 

35from .components.button import Button 

36from .model_proxy import ReviewProxy 

37from .utils import create_new_user, get_review_request_email, send_review_request_email 

38from .views_base import SubmittableModelFormMixin 

39 

40 

41class ReviewCreateView(RoleMixin, CreateView): 

42 """ 

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

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

45 """ 

46 

47 model = Review 

48 form_class = ReviewCreateForm 

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

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

51 version: SubmissionVersion 

52 restricted_roles = [JournalManager, Editor] 

53 

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

55 round_pk = kwargs["version_pk"] 

56 self.version = get_object_or_404(SubmissionVersion, pk=round_pk) 

57 

58 return not self.role_handler.check_rights("can_invite_reviewer", self.version) 

59 

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

61 kwargs = super().get_form_kwargs() 

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

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

64 kwargs["_choices"] = choices 

65 kwargs["_version"] = self.version 

66 # # submission = self.version.submission 

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

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

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

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

71 # 

72 # # current_reviewers = list( 

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

74 # # ) 

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

76 

77 return kwargs 

78 

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

80 return { 

81 "request_email": get_review_request_email( 

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

83 ), 

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

85 "quick_request_email": get_review_request_email( 

86 self.version.submission, 

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

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

89 ), 

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

91 } 

92 

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

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

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

96 context["version"] = self.version 

97 

98 submission = self.version.submission 

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

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

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

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

103 

104 shortlist_url = reverse_lazy("mesh:submission_shortlist", kwargs={"pk": submission.pk}) 

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

106 context["submission"] = submission 

107 

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

109 context["suggestions"] = self.suggestions 

110 

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

112 context["existing_reviewers"] = self.existing_reviewers 

113 

114 choices = [] 

115 has_selected_choice = False 

116 for suggestion in self.suggestions: 

117 suggested_user = ( 

118 suggestion.suggested_user 

119 if suggestion.suggested_user is not None 

120 else suggestion.suggested_reviewer 

121 ) 

122 if suggested_user in self.existing_reviewers: 

123 choices.append( 

124 { 

125 "value": suggestion.pk, 

126 "label": str(suggested_user), 

127 "disabled": True, 

128 "avoid": suggestion.suggest_to_avoid, 

129 } 

130 ) 

131 else: 

132 choice = { 

133 "value": suggestion.pk, 

134 "label": str(suggested_user), 

135 "avoid": suggestion.suggest_to_avoid, 

136 } 

137 if not has_selected_choice: 

138 has_selected_choice = True 

139 choice["selected"] = True 

140 choices.append(choice) 

141 context["suggested_users"] = choices 

142 

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

144 

145 # Generate breadcrumb data 

146 breadcrumb = get_submission_breadcrumb(submission) 

147 breadcrumb.add_item( 

148 title=_("Request review"), 

149 url=reverse_lazy("mesh:review_create", kwargs={"version_pk": self.version.pk}), 

150 ) 

151 context["breadcrumb"] = breadcrumb 

152 

153 return context 

154 

155 def form_valid(self, form: ReviewCreateForm) -> HttpResponse: 

156 """ 

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

158 """ 

159 form.instance.version = self.version 

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

161 

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

163 reviewer = None 

164 suggestion = None 

165 old_suggested_user = None 

166 email = first_name = last_name = "" 

167 

168 reviewer_select = form.cleaned_data["reviewer_select"] 

169 if reviewer_select == "shortlist": 

170 # someone from the shortlist was selected. 

171 # Get the associated suggestion 

172 suggestion_pk = form.cleaned_data["suggested_user"] 

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

174 

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

176 if suggestion.suggested_user is not None: 

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

178 reviewer = suggestion.suggested_user 

179 else: 

180 old_suggested_user = suggestion.suggested_reviewer 

181 first_name = suggestion.suggested_reviewer.first_name 

182 last_name = suggestion.suggested_reviewer.last_name 

183 email = suggestion.suggested_reviewer.email 

184 else: 

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

186 first_name = form.cleaned_data["reviewer_first_name"] 

187 last_name = form.cleaned_data["reviewer_last_name"] 

188 email = form.cleaned_data["reviewer_email"] 

189 

190 if reviewer is None: 

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

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

193 qs = User.objects.filter(email=email) 

194 if qs.exists(): 

195 reviewer = qs.first() 

196 else: 

197 reviewer = create_new_user(email, first_name, last_name) 

198 

199 suggestions_to_update = Suggestion.objects.filter(suggested_reviewer__email=email) 

200 for suggestion_to_update in suggestions_to_update: 

201 suggestion_to_update.suggested_reviewer = None 

202 suggestion_to_update.suggested_user = reviewer 

203 suggestion_to_update.save() 

204 

205 if old_suggested_user is not None: 

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

207 reviewer.keywords = old_suggested_user.keywords 

208 reviewer.save() 

209 

210 old_suggested_user.delete() 

211 

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

213 form.instance.reviewer = reviewer 

214 

215 is_quick_review = form.cleaned_data["quick"] 

216 if is_quick_review: 

217 form.instance.request_email = form.cleaned_data["quick_request_email"] 

218 

219 response = super().form_valid(form) 

220 

221 review: Review = self.object # type:ignore 

222 email_subject = ( 

223 form.cleaned_data["quick_request_email_subject"] 

224 if is_quick_review 

225 else form.cleaned_data["request_email_subject"] 

226 ) 

227 email_content = ( 

228 form.cleaned_data["quick_request_email"] 

229 if is_quick_review 

230 else form.cleaned_data["request_email"] 

231 ) 

232 

233 send_review_request_email( 

234 review, 

235 email_content, 

236 email_subject, 

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

238 ) 

239 

240 messages.success( 

241 self.request, 

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

243 ) 

244 

245 # SubmissionLog.add_message( 

246 # self.version.submission, 

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

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

249 # user=self.request.user, 

250 # significant=True, 

251 # ) 

252 

253 return response 

254 

255 def get_success_url(self) -> str: 

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

257 

258 

259class ReviewEditBaseView(RoleMixin, UpdateView): 

260 """ 

261 Base view for a reviewer to edit a review. 

262 """ 

263 

264 model = Review 

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

266 review: Review 

267 restricted_roles = [Reviewer] 

268 

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

270 pk = kwargs["pk"] 

271 self.review = get_object_or_404(Review, pk=pk) 

272 

273 return not self.role_handler.check_rights("can_edit_review", self.review) 

274 

275 def get_object(self, *args, **kwargs) -> Review: 

276 """ 

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

278 we already have the object loaded. 

279 """ 

280 return self.review 

281 

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

283 return { 

284 "date_response_due_display": self.review.date_response_due, 

285 "date_review_due_display": self.review.date_review_due, 

286 } 

287 

288 

289class ReviewAcceptView(ReviewEditBaseView): 

290 """ 

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

292 """ 

293 

294 form_class = ReviewAcceptForm 

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

296 

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

298 initial = super().get_initial() 

299 return initial 

300 

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

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

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

304 

305 submission = self.review.version.submission 

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

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

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

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

310 description_2 = "Please fill the below form." 

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

312 

313 # Generate breadcrumb data 

314 breadcrumb = get_submission_breadcrumb(submission) 

315 breadcrumb.add_item( 

316 title=_("Accept review"), 

317 url=reverse_lazy("mesh:review_accept", kwargs={"pk": self.review.pk}), 

318 ) 

319 context["breadcrumb"] = breadcrumb 

320 

321 return context 

322 

323 def get_success_url(self) -> str: 

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

325 return reverse_lazy("mesh:review_update", kwargs={"pk": self.review.pk}) 

326 

327 return reverse_lazy( 

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

329 ) 

330 

331 def form_valid(self, form: ReviewAcceptForm) -> HttpResponse: 

332 accept_comment = form.cleaned_data["accept_comment"] 

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

334 

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

336 

337 return HttpResponseRedirect(self.get_success_url()) 

338 

339 

340class ReviewDeclineView(ReviewEditBaseView): 

341 """ 

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

343 """ 

344 

345 form_class = ReviewDeclineForm 

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

347 

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

349 initial = super().get_initial() 

350 return initial 

351 

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

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

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

355 

356 submission = self.review.version.submission 

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

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

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

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

361 description_2 = "Please fill the below form." 

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

363 

364 # Generate breadcrumb data 

365 breadcrumb = get_submission_breadcrumb(submission) 

366 breadcrumb.add_item( 

367 title=_("Decline review"), 

368 url=reverse_lazy("mesh:review_decline", kwargs={"pk": self.review.pk}), 

369 ) 

370 context["breadcrumb"] = breadcrumb 

371 

372 return context 

373 

374 def get_success_url(self) -> str: 

375 return reverse_lazy( 

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

377 ) 

378 

379 def form_valid(self, form: ReviewAcceptForm) -> HttpResponse: 

380 person_info = { 

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

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

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

384 } 

385 submission = self.review.version.submission 

386 add_suggestion_from_person(submission, person_info, suggest_to_avoid=False) 

387 

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

389 

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

391 

392 return HttpResponseRedirect(self.get_success_url()) 

393 

394 

395class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView): 

396 """ 

397 View for performing a review on a submission version. 

398 """ 

399 

400 form_class = ReviewSubmitForm 

401 add_confirm_message = False 

402 

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

404 """ 

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

406 """ 

407 if self.review.state in [ 

408 ReviewState.AWAITING_ACCEPTANCE.value, 

409 ReviewState.DECLINED.value, 

410 ]: 

411 # Forward all query parameters 

412 return HttpResponseRedirect( 

413 format_url_with_params( 

414 reverse("mesh:review_accept", kwargs={"pk": self.review.pk}), 

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

416 ) 

417 ) 

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

419 

420 def get_success_url(self) -> str: 

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

422 return self.submit_url() 

423 return reverse_lazy( 

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

425 ) 

426 

427 def submit_url(self) -> str: 

428 return reverse_lazy("mesh:review_confirm", kwargs={"pk": self.review.pk}) 

429 

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

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

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

433 

434 submission = self.review.version.submission 

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

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

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

438 description += "of submission " 

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

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

441 context["form_description"] = [description] 

442 

443 # Generate breadcrumb data 

444 breadcrumb = get_submission_breadcrumb(submission) 

445 breadcrumb.add_item( 

446 title=_("Submit review"), 

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

448 ) 

449 context["breadcrumb"] = breadcrumb 

450 

451 return context 

452 

453 def form_pre_save(self, form: ReviewSubmitForm) -> None: 

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

455 

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

457 """ 

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

459 """ 

460 deletion_requested, _ = post_delete_model_file(self.review, request, self.role_handler) 

461 

462 if deletion_requested: 

463 self.review = get_object_or_404(Review, pk=self.review.pk) 

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

465 

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

467 

468 

469class ReviewConfirmView(RoleMixin, FormView): 

470 """ 

471 View to confirm the submission of a review. 

472 """ 

473 

474 review: Review 

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

476 form_class = ReviewConfirmForm 

477 

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

479 pk = kwargs["pk"] 

480 self.review = get_object_or_404(Review, pk=pk) 

481 

482 return not self.role_handler.check_rights("can_submit_review", self.review) 

483 

484 def get_success_url(self): 

485 return reverse_lazy( 

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

487 ) 

488 

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

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

491 

492 context["form"].buttons = [ 

493 Button( 

494 id="form_save", 

495 title=_("Submit"), 

496 icon_class="fa-check", 

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

498 ) 

499 ] 

500 

501 context["review_proxy"] = ReviewProxy(self.review, self.role_handler) 

502 

503 files = [] 

504 for file_wrapper in self.review.additional_files.all(): # type:ignore 

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

506 context["review_files"] = files 

507 

508 # Breadcrumb 

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

510 breadcrumb.add_item( 

511 title=_("Submit review"), 

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

513 ) 

514 context["breadcrumb"] = breadcrumb 

515 

516 return context 

517 

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

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

520 

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

522 

523 return HttpResponseRedirect(self.get_success_url()) 

524 

525 

526REVIEW_FILE_INPUT_PREFIX = "review_file_" 

527REVIEW_FILE_INPUT_VALUE = "true" 

528 

529 

530class ReviewDetails(RoleMixin, TemplateView): 

531 """ 

532 Details view for a review. 

533 """ 

534 

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

536 review: Review 

537 

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

539 self.review = get_object_or_404(Review, pk=kwargs["pk"]) 

540 

541 return not self.role_handler.check_rights("can_access_review", self.review) 

542 

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

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

545 context["review_proxy"] = ReviewProxy(self.review, self.role_handler) 

546 context["input_prefix"] = REVIEW_FILE_INPUT_PREFIX 

547 context["input_value"] = REVIEW_FILE_INPUT_VALUE 

548 

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

550 breadcrumb.add_item( 

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

552 url=reverse_lazy("mesh:review_details", kwargs={"pk": self.review.pk}), 

553 ) 

554 context["breadcrumb"] = breadcrumb 

555 return context 

556 

557 

558class ReviewFileAccessUpdate(RoleMixin, View): 

559 """ 

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

561 """ 

562 

563 review: Review 

564 form_cache: str 

565 

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

567 self.review = get_object_or_404(Review, pk=kwargs["pk"]) 

568 

569 return not self.role_handler.check_rights("can_edit_review_file_right", self.review) 

570 

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

572 response = HttpResponseRedirect( 

573 reverse_lazy("mesh:review_details", kwargs={"pk": self.review.pk}) 

574 ) 

575 files: QuerySet[ReviewAdditionalFile] = self.review.additional_files.all() # type:ignore 

576 if not files: 

577 return response 

578 files: dict[int, ReviewAdditionalFile] = {f.pk: f for f in files} # type:ignore 

579 

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

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

582 files_handled: list[int] = [] 

583 files_changed: list[ReviewAdditionalFile] = [] 

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

585 field_match = field_regex.match(field) 

586 if not field_match: 

587 continue 

588 if values[0] != REVIEW_FILE_INPUT_VALUE: 

589 continue 

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

591 file = files.get(file_pk) 

592 if file is None: 

593 continue 

594 # The boolean already has the correct value 

595 if file.author_access: 

596 files_handled.append(file_pk) 

597 continue 

598 file.author_access = True 

599 file.save() 

600 files_handled.append(file_pk) 

601 files_changed.append(file) 

602 

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

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

605 if file_pk not in files_handled: 

606 if not file.author_access: 

607 continue 

608 file.author_access = False 

609 file.save() 

610 files_changed.append(file) 

611 

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

613 

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

615 if changed_files_str: 

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

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

618 SubmissionLog.add_message( 

619 self.review.version.submission, 

620 content=message, 

621 content_en=message, 

622 user=self.request.user, 

623 significant=True, 

624 ) 

625 

626 return response 

627 

628 

629class ReviewFileAccessAPIView(RoleMixin, View): 

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

631 file = get_object_or_404(ReviewAdditionalFile, pk=kwargs["pk"]) 

632 file.author_access = not file.author_access 

633 file.save() 

634 

635 return JsonResponse({}) 

636 

637 

638class ReviewAutoCreateView(RoleMixin, CreateView): 

639 """ 

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

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

642 """ 

643 

644 model = Review 

645 form_class = ReviewAutoCreateForm 

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

647 version: SubmissionVersion 

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

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

650 restricted_roles = [JournalManager, Editor] 

651 

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

653 round_pk = kwargs["version_pk"] 

654 self.version = get_object_or_404(SubmissionVersion, pk=round_pk) 

655 

656 return not self.role_handler.check_rights("can_invite_reviewer", self.version) 

657 

658 def init_reviewers(self): 

659 # Reviewers already assigned to this round 

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

661 

662 submission = self.version.submission 

663 previous_round_number = self.version.number - 1 

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

665 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

666 state=ReviewState.SUBMITTED.value 

667 ).exclude( 

668 recommendation__in=[ 

669 RecommendationValue.REJECTED.value, 

670 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

671 ] 

672 ) 

673 

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

675 # not already assigned to this round. 

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

677 self.previous_reviewers = [ 

678 review.reviewer 

679 for review in reviews_that_can_be_auto_reassigned 

680 if review.reviewer not in self.existing_reviewers 

681 ] 

682 

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

684 self.init_reviewers() 

685 return super().get(self, request, *args, **kwargs) 

686 

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

688 self.init_reviewers() 

689 return super().post(self, request, *args, **kwargs) 

690 

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

692 return { 

693 "request_email": get_review_request_email( 

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

695 ), 

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

697 } 

698 

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

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

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

702 

703 submission = self.version.submission 

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

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

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

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

708 

709 context["submission"] = submission 

710 

711 # Reviewers already assigned to this round 

712 context["existing_reviewers"] = self.existing_reviewers 

713 

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

715 # not already assigned to this round. 

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

717 context["previous_reviewers"] = self.previous_reviewers 

718 

719 context["form_description"] = [description] 

720 

721 # Generate breadcrumb data 

722 breadcrumb = get_submission_breadcrumb(submission) 

723 breadcrumb.add_item( 

724 title=_("Request review"), 

725 url=reverse_lazy("mesh:review_create", kwargs={"version_pk": self.version.pk}), 

726 ) 

727 context["breadcrumb"] = breadcrumb 

728 

729 return context 

730 

731 def form_valid(self, form: ReviewCreateForm) -> HttpResponse: 

732 """ 

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

734 """ 

735 form.instance.version = self.version 

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

737 

738 if len(self.previous_reviewers) > 0: 

739 today = datetime.date.today() 

740 

741 reviewer = self.previous_reviewers[0] 

742 form.instance.reviewer = reviewer 

743 form.instance.date_response_due = today 

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

745 response = super().form_valid(form) 

746 

747 review = self.object 

748 send_review_request_email( 

749 review, 

750 form.cleaned_data["request_email"], 

751 form.cleaned_data["request_email_subject"], 

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

753 ) 

754 

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

756 review = Review( 

757 version=self.version, 

758 reviewer=reviewer, 

759 date_response_due=today, 

760 date_review_due=form.instance.date_review_due, 

761 state=ReviewState.PENDING.value, 

762 request_email=form.instance.request_email, 

763 ) 

764 review._user = self.request.user 

765 review.save() 

766 

767 send_review_request_email( 

768 review, 

769 form.cleaned_data["request_email"], 

770 form.cleaned_data["request_email_subject"], 

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

772 ) 

773 

774 messages.success( 

775 self.request, 

776 _("Referee request successfully submitted"), 

777 ) 

778 

779 # SubmissionLog.add_message( 

780 # self.version.submission, 

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

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

783 # user=self.request.user, 

784 # significant=True, 

785 # ) 

786 

787 else: 

788 response = super().form_invalid(form) 

789 

790 return response 

791 

792 def get_success_url(self) -> str: 

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