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
« prev ^ index » next coverage.py v7.9.0, created at 2025-09-10 11:20 +0000
1import datetime
2import re
3from typing import Any
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
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
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
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 """
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]
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)
58 return not self.role_handler.check_rights("can_invite_reviewer", self.version)
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)
77 return kwargs
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 }
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
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>."
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
108 self.suggestions = Suggestion.objects.filter(submission=submission).order_by("seq")
109 context["suggestions"] = self.suggestions
111 self.existing_reviewers = [review.reviewer for review in self.version.reviews.all()]
112 context["existing_reviewers"] = self.existing_reviewers
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
143 context["form_description"] = [description, action]
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
153 return context
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
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 = ""
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))
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"]
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)
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()
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()
210 old_suggested_user.delete()
212 # We now know the reviewer. Set the form.instance so that Django can create a Review
213 form.instance.reviewer = reviewer
215 is_quick_review = form.cleaned_data["quick"]
216 if is_quick_review:
217 form.instance.request_email = form.cleaned_data["quick_request_email"]
219 response = super().form_valid(form)
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 )
233 send_review_request_email(
234 review,
235 email_content,
236 email_subject,
237 self.request.build_absolute_uri("/"),
238 )
240 messages.success(
241 self.request,
242 _(f"Referee request successfully submitted to {review.reviewer}"),
243 )
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 # )
253 return response
255 def get_success_url(self) -> str:
256 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})
259class ReviewEditBaseView(RoleMixin, UpdateView):
260 """
261 Base view for a reviewer to edit a review.
262 """
264 model = Review
265 template_name = "mesh/forms/form_full_page.html"
266 review: Review
267 restricted_roles = [Reviewer]
269 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
270 pk = kwargs["pk"]
271 self.review = get_object_or_404(Review, pk=pk)
273 return not self.role_handler.check_rights("can_edit_review", self.review)
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
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 }
289class ReviewAcceptView(ReviewEditBaseView):
290 """
291 Preliminary view for a reviewer to accept a requested review.
292 """
294 form_class = ReviewAcceptForm
295 template_name = "mesh/review/review_accept.html"
297 def get_initial(self) -> dict[str, Any]:
298 initial = super().get_initial()
299 return initial
301 def get_context_data(self, *args, **kwargs):
302 context = super().get_context_data(*args, **kwargs)
303 context["page_title"] = "Accept review request"
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]
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
321 return context
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})
327 return reverse_lazy(
328 "mesh:submission_details", kwargs={"pk": self.review.version.submission.pk}
329 )
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)
335 messages.success(self.request, _("Review request successfully accepted"))
337 return HttpResponseRedirect(self.get_success_url())
340class ReviewDeclineView(ReviewEditBaseView):
341 """
342 Preliminary view for a reviewer to decline a requested review.
343 """
345 form_class = ReviewDeclineForm
346 template_name = "mesh/review/review_decline.html"
348 def get_initial(self) -> dict[str, Any]:
349 initial = super().get_initial()
350 return initial
352 def get_context_data(self, *args, **kwargs):
353 context = super().get_context_data(*args, **kwargs)
354 context["page_title"] = "Decline review request"
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]
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
372 return context
374 def get_success_url(self) -> str:
375 return reverse_lazy(
376 "mesh:submission_details", kwargs={"pk": self.review.version.submission.pk}
377 )
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)
388 self.review.accept(accept_value=False, accept_comment="", user=self.request.user)
390 messages.success(self.request, _("Review request successfully declined"))
392 return HttpResponseRedirect(self.get_success_url())
395class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView):
396 """
397 View for performing a review on a submission version.
398 """
400 form_class = ReviewSubmitForm
401 add_confirm_message = False
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)
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 )
427 def submit_url(self) -> str:
428 return reverse_lazy("mesh:review_confirm", kwargs={"pk": self.review.pk})
430 def get_context_data(self, *args, **kwargs):
431 context = super().get_context_data(*args, **kwargs)
432 context["page_title"] = "Submit review"
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]
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
451 return context
453 def form_pre_save(self, form: ReviewSubmitForm) -> None:
454 form.instance._user = self.request.user
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)
462 if deletion_requested:
463 self.review = get_object_or_404(Review, pk=self.review.pk)
464 return self.get(request, *args, **kwargs)
466 return super().post(request, *args, **kwargs)
469class ReviewConfirmView(RoleMixin, FormView):
470 """
471 View to confirm the submission of a review.
472 """
474 review: Review
475 template_name = "mesh/review/review_confirm.html"
476 form_class = ReviewConfirmForm
478 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
479 pk = kwargs["pk"]
480 self.review = get_object_or_404(Review, pk=pk)
482 return not self.role_handler.check_rights("can_submit_review", self.review)
484 def get_success_url(self):
485 return reverse_lazy(
486 "mesh:submission_details", kwargs={"pk": self.review.version.submission.pk}
487 )
489 def get_context_data(self, *args, **kwargs):
490 context = super().get_context_data(*args, **kwargs)
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 ]
501 context["review_proxy"] = ReviewProxy(self.review, self.role_handler)
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
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
516 return context
518 def form_valid(self, form: ReviewConfirmForm) -> HttpResponse:
519 self.review.submit(self.request.user)
521 messages.success(self.request, _("Your review has been successfully submitted."))
523 return HttpResponseRedirect(self.get_success_url())
526REVIEW_FILE_INPUT_PREFIX = "review_file_"
527REVIEW_FILE_INPUT_VALUE = "true"
530class ReviewDetails(RoleMixin, TemplateView):
531 """
532 Details view for a review.
533 """
535 template_name = "mesh/review/review_details.html"
536 review: Review
538 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
539 self.review = get_object_or_404(Review, pk=kwargs["pk"])
541 return not self.role_handler.check_rights("can_access_review", self.review)
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
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
558class ReviewFileAccessUpdate(RoleMixin, View):
559 """
560 View to edit the author access to a review's files.
561 """
563 review: Review
564 form_cache: str
566 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
567 self.review = get_object_or_404(Review, pk=kwargs["pk"])
569 return not self.role_handler.check_rights("can_edit_review_file_right", self.review)
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
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)
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)
612 messages.success(request, _("Successfully updated the author access right."))
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 )
626 return response
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()
635 return JsonResponse({})
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 """
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]
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)
656 return not self.role_handler.check_rights("can_invite_reviewer", self.version)
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()]
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 )
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 ]
683 def get(self, request, *args, **kwargs):
684 self.init_reviewers()
685 return super().get(self, request, *args, **kwargs)
687 def post(self, request, *args, **kwargs):
688 self.init_reviewers()
689 return super().post(self, request, *args, **kwargs)
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 }
699 def get_context_data(self, *args, **kwargs):
700 context = super().get_context_data(*args, **kwargs)
701 context["page_title"] = "Request review"
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>."
709 context["submission"] = submission
711 # Reviewers already assigned to this round
712 context["existing_reviewers"] = self.existing_reviewers
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
719 context["form_description"] = [description]
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
729 return context
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
738 if len(self.previous_reviewers) > 0:
739 today = datetime.date.today()
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)
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 )
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()
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 )
774 messages.success(
775 self.request,
776 _("Referee request successfully submitted"),
777 )
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 # )
787 else:
788 response = super().form_invalid(form)
790 return response
792 def get_success_url(self) -> str:
793 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})