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

362 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-03 13:52 +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 ReviewSubmitForm, 

26) 

27from mesh.views.mixins import RoleMixin 

28 

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

30from ..models.submission_models import SubmissionLog, SubmissionVersion 

31from ..models.user_models import Suggestion, User 

32from .components.breadcrumb import get_submission_breadcrumb 

33from .components.button import Button 

34from .model_proxy import ReviewProxy 

35from .utils import create_new_user, get_review_request_email, send_review_request_email 

36from .views_base import SubmittableModelFormMixin 

37 

38 

39class ReviewCreateView(RoleMixin, CreateView): 

40 """ 

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

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

43 """ 

44 

45 model = Review 

46 form_class = ReviewCreateForm 

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

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

49 version: SubmissionVersion 

50 restricted_roles = [JournalManager, Editor] 

51 

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

53 round_pk = kwargs["version_pk"] 

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

55 

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

57 

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

59 kwargs = super().get_form_kwargs() 

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

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

62 kwargs["_choices"] = choices 

63 kwargs["_version"] = self.version 

64 # # submission = self.version.submission 

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

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

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

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

69 # 

70 # # current_reviewers = list( 

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

72 # # ) 

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

74 

75 return kwargs 

76 

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

78 return { 

79 "request_email": get_review_request_email( 

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

81 ), 

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

83 } 

84 

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

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

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

88 

89 submission = self.version.submission 

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

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

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

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

94 

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

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

97 context["submission"] = submission 

98 

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

100 context["suggestions"] = self.suggestions 

101 

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

103 context["existing_reviewers"] = self.existing_reviewers 

104 

105 choices = [] 

106 has_selected_choice = False 

107 for suggestion in self.suggestions: 

108 suggested_user = ( 

109 suggestion.suggested_user 

110 if suggestion.suggested_user is not None 

111 else suggestion.suggested_reviewer 

112 ) 

113 if suggested_user in self.existing_reviewers: 

114 choices.append( 

115 {"value": suggestion.pk, "label": str(suggested_user), "disabled": True} 

116 ) 

117 else: 

118 choice = {"value": suggestion.pk, "label": str(suggested_user)} 

119 if not has_selected_choice: 

120 has_selected_choice = True 

121 choice["selected"] = True 

122 choices.append(choice) 

123 context["suggested_users"] = choices 

124 

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

126 

127 # Generate breadcrumb data 

128 breadcrumb = get_submission_breadcrumb(submission) 

129 breadcrumb.add_item( 

130 title=_("Request review"), 

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

132 ) 

133 context["breadcrumb"] = breadcrumb 

134 

135 return context 

136 

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

138 """ 

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

140 """ 

141 form.instance.version = self.version 

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

143 

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

145 reviewer = None 

146 suggestion = None 

147 old_suggested_user = None 

148 email = first_name = last_name = "" 

149 

150 reviewer_select = form.cleaned_data["reviewer_select"] 

151 if reviewer_select == "shortlist": 

152 # someone from the shortlist was selected. 

153 # Get the associated suggestion 

154 suggestion_pk = form.cleaned_data["suggested_user"] 

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

156 

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

158 if suggestion.suggested_user is not None: 

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

160 reviewer = suggestion.suggested_user 

161 else: 

162 old_suggested_user = suggestion.suggested_reviewer 

163 first_name = suggestion.suggested_reviewer.first_name 

164 last_name = suggestion.suggested_reviewer.last_name 

165 email = suggestion.suggested_reviewer.email 

166 else: 

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

168 first_name = form.cleaned_data["reviewer_first_name"] 

169 last_name = form.cleaned_data["reviewer_last_name"] 

170 email = form.cleaned_data["reviewer_email"] 

171 

172 if reviewer is None: 

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

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

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

176 if qs.exists(): 

177 reviewer = qs.first() 

178 else: 

179 reviewer = create_new_user(email, first_name, last_name) 

180 

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

182 for suggestion_to_update in suggestions_to_update: 

183 suggestion_to_update.suggested_reviewer = None 

184 suggestion_to_update.suggested_user = reviewer 

185 suggestion_to_update.save() 

186 

187 if old_suggested_user is not None: 

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

189 reviewer.keywords = old_suggested_user.keywords 

190 reviewer.save() 

191 

192 old_suggested_user.delete() 

193 

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

195 form.instance.reviewer = reviewer 

196 

197 response = super().form_valid(form) 

198 

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

200 send_review_request_email( 

201 review, 

202 form.cleaned_data["request_email"], 

203 form.cleaned_data["request_email_subject"], 

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

205 ) 

206 

207 messages.success( 

208 self.request, 

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

210 ) 

211 

212 # SubmissionLog.add_message( 

213 # self.version.submission, 

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

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

216 # user=self.request.user, 

217 # significant=True, 

218 # ) 

219 

220 return response 

221 

222 def get_success_url(self) -> str: 

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

224 

225 

226class ReviewEditBaseView(RoleMixin, UpdateView): 

227 """ 

228 Base view for a reviewer to edit a review. 

229 """ 

230 

231 model = Review 

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

233 review: Review 

234 restricted_roles = [Reviewer] 

235 

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

237 pk = kwargs["pk"] 

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

239 

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

241 

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

243 """ 

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

245 we already have the object loaded. 

246 """ 

247 return self.review 

248 

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

250 return { 

251 "date_response_due_display": self.review.date_response_due, 

252 "date_review_due_display": self.review.date_review_due, 

253 } 

254 

255 

256class ReviewAcceptView(ReviewEditBaseView): 

257 """ 

258 Preliminary view for a reviewer to accept/decline a requested review. 

259 """ 

260 

261 form_class = ReviewAcceptForm 

262 

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

264 initial = super().get_initial() 

265 accept_initial = self.request.GET.get("accepted", None) 

266 if accept_initial == "true": 

267 initial["accepted"] = True 

268 elif accept_initial == "false": 

269 initial["accepted"] = False 

270 return initial 

271 

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

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

274 context["page_title"] = "Accept/decline review request" 

275 

276 submission = self.review.version.submission 

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

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

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

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

281 description_2 = "Please accept or decline the review request by filling the below form." 

282 description_3 = "You must accept the request before proceeding with the actual review." 

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

284 

285 # Generate breadcrumb data 

286 breadcrumb = get_submission_breadcrumb(submission) 

287 breadcrumb.add_item( 

288 title=_("Accept review"), 

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

290 ) 

291 context["breadcrumb"] = breadcrumb 

292 

293 return context 

294 

295 def get_success_url(self) -> str: 

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

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

298 

299 return reverse_lazy( 

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

301 ) 

302 

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

304 accept_value = form.cleaned_data["accepted"] 

305 accept_comment = form.cleaned_data["accept_comment"] 

306 

307 self.review.accept(accept_value, accept_comment, user=self.request.user) 

308 

309 messages.success( 

310 self.request, 

311 ( 

312 _("Review request successfully accepted") 

313 if accept_value 

314 else _("Review request successfully declined") 

315 ), 

316 ) 

317 

318 return HttpResponseRedirect(self.get_success_url()) 

319 

320 

321class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView): 

322 """ 

323 View for performing a review on a submission version. 

324 """ 

325 

326 form_class = ReviewSubmitForm 

327 add_confirm_message = False 

328 

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

330 """ 

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

332 """ 

333 if self.review.state in [ 

334 ReviewState.AWAITING_ACCEPTANCE.value, 

335 ReviewState.DECLINED.value, 

336 ]: 

337 # Forward all query parameters 

338 return HttpResponseRedirect( 

339 format_url_with_params( 

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

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

342 ) 

343 ) 

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

345 

346 def get_success_url(self) -> str: 

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

348 return self.submit_url() 

349 return reverse_lazy( 

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

351 ) 

352 

353 def submit_url(self) -> str: 

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

355 

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

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

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

359 

360 submission = self.review.version.submission 

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

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

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

364 description += "of submission " 

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

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

367 context["form_description"] = [description] 

368 

369 # Generate breadcrumb data 

370 breadcrumb = get_submission_breadcrumb(submission) 

371 breadcrumb.add_item( 

372 title=_("Submit review"), 

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

374 ) 

375 context["breadcrumb"] = breadcrumb 

376 

377 return context 

378 

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

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

381 

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

383 """ 

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

385 """ 

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

387 

388 if deletion_requested: 

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

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

391 

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

393 

394 

395class ReviewConfirmView(RoleMixin, FormView): 

396 """ 

397 View to confirm the submission of a review. 

398 """ 

399 

400 review: Review 

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

402 form_class = ReviewConfirmForm 

403 

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

405 pk = kwargs["pk"] 

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

407 

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

409 

410 def get_success_url(self): 

411 return reverse_lazy( 

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

413 ) 

414 

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

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

417 

418 context["form"].buttons = [ 

419 Button( 

420 id="form_save", 

421 title=_("Submit"), 

422 icon_class="fa-check", 

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

424 ) 

425 ] 

426 

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

428 

429 files = [] 

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

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

432 context["review_files"] = files 

433 

434 # Breadcrumb 

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

436 breadcrumb.add_item( 

437 title=_("Submit review"), 

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

439 ) 

440 context["breadcrumb"] = breadcrumb 

441 

442 return context 

443 

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

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

446 

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

448 

449 return HttpResponseRedirect(self.get_success_url()) 

450 

451 

452REVIEW_FILE_INPUT_PREFIX = "review_file_" 

453REVIEW_FILE_INPUT_VALUE = "true" 

454 

455 

456class ReviewDetails(RoleMixin, TemplateView): 

457 """ 

458 Details view for a review. 

459 """ 

460 

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

462 review: Review 

463 

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

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

466 

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

468 

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

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

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

472 context["input_prefix"] = REVIEW_FILE_INPUT_PREFIX 

473 context["input_value"] = REVIEW_FILE_INPUT_VALUE 

474 

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

476 breadcrumb.add_item( 

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

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

479 ) 

480 context["breadcrumb"] = breadcrumb 

481 return context 

482 

483 

484class ReviewFileAccessUpdate(RoleMixin, View): 

485 """ 

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

487 """ 

488 

489 review: Review 

490 form_cache: str 

491 

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

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

494 

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

496 

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

498 response = HttpResponseRedirect( 

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

500 ) 

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

502 if not files: 

503 return response 

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

505 

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

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

508 files_handled: list[int] = [] 

509 files_changed: list[ReviewAdditionalFile] = [] 

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

511 field_match = field_regex.match(field) 

512 if not field_match: 

513 continue 

514 if values[0] != REVIEW_FILE_INPUT_VALUE: 

515 continue 

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

517 file = files.get(file_pk) 

518 if file is None: 

519 continue 

520 # The boolean already has the correct value 

521 if file.author_access: 

522 files_handled.append(file_pk) 

523 continue 

524 file.author_access = True 

525 file.save() 

526 files_handled.append(file_pk) 

527 files_changed.append(file) 

528 

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

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

531 if file_pk not in files_handled: 

532 if not file.author_access: 

533 continue 

534 file.author_access = False 

535 file.save() 

536 files_changed.append(file) 

537 

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

539 

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

541 if changed_files_str: 

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

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

544 SubmissionLog.add_message( 

545 self.review.version.submission, 

546 content=message, 

547 content_en=message, 

548 user=self.request.user, 

549 significant=True, 

550 ) 

551 

552 return response 

553 

554 

555class ReviewFileAccessAPIView(RoleMixin, View): 

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

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

558 file.author_access = not file.author_access 

559 file.save() 

560 

561 return JsonResponse({}) 

562 

563 

564class ReviewAutoCreateView(RoleMixin, CreateView): 

565 """ 

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

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

568 """ 

569 

570 model = Review 

571 form_class = ReviewAutoCreateForm 

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

573 version: SubmissionVersion 

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

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

576 restricted_roles = [JournalManager, Editor] 

577 

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

579 round_pk = kwargs["version_pk"] 

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

581 

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

583 

584 def init_reviewers(self): 

585 # Reviewers already assigned to this round 

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

587 

588 submission = self.version.submission 

589 previous_round_number = self.version.number - 1 

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

591 reviews_that_can_be_auto_reassigned = previous_round.reviews.filter( 

592 state=ReviewState.SUBMITTED.value 

593 ).exclude( 

594 recommendation__in=[ 

595 RecommendationValue.REJECTED.value, 

596 RecommendationValue.RESUBMIT_SOMEWHERE_ELSE.value, 

597 ] 

598 ) 

599 

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

601 # not already assigned to this round. 

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

603 self.previous_reviewers = [ 

604 review.reviewer 

605 for review in reviews_that_can_be_auto_reassigned 

606 if review.reviewer not in self.existing_reviewers 

607 ] 

608 

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

610 self.init_reviewers() 

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

612 

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

614 self.init_reviewers() 

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

616 

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

618 return { 

619 "request_email": get_review_request_email( 

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

621 ), 

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

623 } 

624 

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

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

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

628 

629 submission = self.version.submission 

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

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

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

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

634 

635 context["submission"] = submission 

636 

637 # Reviewers already assigned to this round 

638 context["existing_reviewers"] = self.existing_reviewers 

639 

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

641 # not already assigned to this round. 

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

643 context["previous_reviewers"] = self.previous_reviewers 

644 

645 context["form_description"] = [description] 

646 

647 # Generate breadcrumb data 

648 breadcrumb = get_submission_breadcrumb(submission) 

649 breadcrumb.add_item( 

650 title=_("Request review"), 

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

652 ) 

653 context["breadcrumb"] = breadcrumb 

654 

655 return context 

656 

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

658 """ 

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

660 """ 

661 form.instance.version = self.version 

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

663 

664 if len(self.previous_reviewers) > 0: 

665 today = datetime.date.today() 

666 

667 reviewer = self.previous_reviewers[0] 

668 form.instance.reviewer = reviewer 

669 form.instance.date_response_due = today 

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

671 response = super().form_valid(form) 

672 

673 review = self.object 

674 send_review_request_email( 

675 review, 

676 form.cleaned_data["request_email"], 

677 form.cleaned_data["request_email_subject"], 

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

679 ) 

680 

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

682 review = Review( 

683 version=self.version, 

684 reviewer=reviewer, 

685 date_response_due=today, 

686 date_review_due=form.instance.date_review_due, 

687 state=ReviewState.PENDING.value, 

688 request_email=form.instance.request_email, 

689 ) 

690 review._user = self.request.user 

691 review.save() 

692 

693 send_review_request_email( 

694 review, 

695 form.cleaned_data["request_email"], 

696 form.cleaned_data["request_email_subject"], 

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

698 ) 

699 

700 messages.success( 

701 self.request, 

702 _("Referee request successfully submitted"), 

703 ) 

704 

705 # SubmissionLog.add_message( 

706 # self.version.submission, 

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

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

709 # user=self.request.user, 

710 # significant=True, 

711 # ) 

712 

713 else: 

714 response = super().form_invalid(form) 

715 

716 return response 

717 

718 def get_success_url(self) -> str: 

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