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
« prev ^ index » next coverage.py v7.13.1, created at 2026-07-09 13:14 +0000
1import datetime
2import re
3from typing import Any
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
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
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 """
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
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
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)
81 return kwargs
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 }
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
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>."
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
116 self.suggestions = Suggestion.objects.filter(submission=submission).order_by("seq")
117 context["suggestions"] = self.suggestions
119 self.existing_reviewers = [review.reviewer for review in self.version.reviews_censored]
120 context["existing_reviewers"] = self.existing_reviewers
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
151 context["form_description"] = [description, action]
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
167 return context
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 = ""
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))
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"]
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 )
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()
213 old_suggested_user.delete()
215 # We now know the reviewer. Set the form.instance so that Django can create a Review
217 review = send_review_request(self.request, reviewer, self.version, form)
218 # review = send_review_request(self.request.user, reviewer, self.version, form)
220 messages.success(
221 self.request,
222 _(f"Referee request successfully submitted to {review.reviewer}"),
223 )
224 # Redirect
225 return super().form_valid(form)
227 def get_success_url(self) -> str:
228 return reverse(
229 "mesh:submission_details", kwargs={"submission_pk": self.version.submission.pk}
230 )
233class ReviewEditBaseView(LoginRequiredMixin, MeshObjectMixin, UpdateView):
234 """
235 Base view for a reviewer to edit a review.
236 """
238 model = Review
239 template_name = "mesh/forms/form_full_page.html"
240 review: Review
241 restricted_roles = [Reviewer]
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
249 def get_object(self, *args, **kwargs):
250 return self.review
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 }
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]
274 return context
276 def get_success_url(self) -> str:
277 return reverse(
278 "mesh:submission_details", kwargs={"submission_pk": self.review.version.submission.pk}
279 )
282class ReviewAcceptView(ReviewAnswerBaseView):
283 """
284 Preliminary view for a reviewer to accept a requested review.
285 """
287 form_class = ReviewAcceptForm
288 template_name = "mesh/review/review_accept.html"
290 def get_context_data(self, *args, **kwargs):
291 context = super().get_context_data(*args, **kwargs)
292 context["page_title"] = "Accept review request"
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
310 return context
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 )
323 return super().get_success_url()
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)
329 messages.success(self.request, _("Review request successfully accepted"))
331 return HttpResponseRedirect(self.get_success_url())
334class ReviewDeclineView(ReviewAnswerBaseView):
335 """
336 Preliminary view for a reviewer to decline a requested review.
337 """
339 form_class = ReviewDeclineForm
340 template_name = "mesh/review/review_decline.html"
342 def get_context_data(self, *args, **kwargs):
343 context = super().get_context_data(*args, **kwargs)
344 context["page_title"] = "Decline review request"
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
362 return context
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)
373 self.review.accept(accept_value=False, accept_comment="", user=self.request.user)
375 messages.success(self.request, _("Review request successfully declined"))
377 return HttpResponseRedirect(self.get_success_url())
380class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView):
381 """
382 View for performing a review on a submission version.
383 """
385 form_class = ReviewSubmitForm
386 add_confirm_message = False
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)
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 )
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 )
429 def get_context_data(self, *args, **kwargs):
430 context = super().get_context_data(*args, **kwargs)
431 context["page_title"] = "Submit review"
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]
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
459 return context
461 def form_pre_save(self, form) -> None:
462 form.instance._user = self.request.user
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 )
472 if deletion_requested:
473 self.review = self.get_review()
474 return self.get(request, *args, **kwargs)
476 return super().post(request, *args, **kwargs)
479class ReviewConfirmView(LoginRequiredMixin, MeshObjectMixin, FormView):
480 """
481 View to confirm the submission of a review.
482 """
484 review: Review
485 template_name = "mesh/review/review_confirm.html"
486 form_class = ReviewConfirmForm
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
496 def get_success_url(self):
497 return reverse(
498 "mesh:submission_details", kwargs={"submission_pk": self.review.version.submission.pk}
499 )
501 def get_context_data(self, *args, **kwargs):
502 context = super().get_context_data(*args, **kwargs)
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 ]
513 context["review_proxy"] = ReviewProxy(
514 self.review, self.request.current_role, version=self.version
515 )
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
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
537 return context
539 def form_valid(self, form: ReviewConfirmForm) -> HttpResponse:
540 self.review.submit(self.request.user)
541 # self.review.submit()
543 messages.success(self.request, _("Your review has been successfully submitted."))
545 return HttpResponseRedirect(self.get_success_url())
548REVIEW_FILE_INPUT_PREFIX = "review_file_"
549REVIEW_FILE_INPUT_VALUE = "true"
552class ReviewDetails(LoginRequiredMixin, MeshObjectMixin, TemplateView):
553 """
554 Details view for a review.
555 """
557 template_name = "mesh/review/review_details.html"
558 review: Review
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
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
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
592class ReviewFileAccessUpdate(LoginRequiredMixin, MeshObjectMixin, View):
593 """
594 View to edit the author access to a review's files.
595 """
597 form_cache: str
599 def setup(self, request, *args, **kwargs):
600 super().setup(request, *args, **kwargs)
601 review = self.get_review()
603 if not self.request.current_role.can_edit_review_file_right(review):
604 raise PermissionError
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}
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)
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)
658 messages.success(request, _("Successfully updated the author access right."))
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 )
672 return response
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 """
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]
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
695 # Reviewers already assigned to this round
696 self.existing_reviewers = [review.reviewer for review in self.version.reviews_censored]
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 )
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 ]
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 }
727 def get_context_data(self, *args, **kwargs):
728 context = super().get_context_data(*args, **kwargs)
729 context["page_title"] = "Request review"
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>."
739 context["submission"] = submission
741 # Reviewers already assigned to this round
742 context["existing_reviewers"] = self.existing_reviewers
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
749 context["form_description"] = [description]
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
765 return context
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
774 if len(self.previous_reviewers) > 0:
775 today = datetime.date.today()
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)
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 )
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()
802 send_review_request_email(
803 review,
804 form.cleaned_data["request_email"],
805 form.cleaned_data["request_email_subject"],
806 )
808 messages.success(
809 self.request,
810 _("Referee request successfully submitted"),
811 )
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 # )
821 else:
822 response = super().form_invalid(form)
824 return response
826 def get_success_url(self) -> str:
827 return reverse(
828 "mesh:submission_details", kwargs={"submission_pk": self.version.submission.pk}
829 )