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
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-03 13:52 +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 ReviewSubmitForm,
26)
27from mesh.views.mixins import RoleMixin
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
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 """
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]
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)
56 return not self.role_handler.check_rights("can_invite_reviewer", self.version)
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)
75 return kwargs
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 }
85 def get_context_data(self, *args, **kwargs):
86 context = super().get_context_data(*args, **kwargs)
87 context["page_title"] = "Request review"
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>."
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
99 self.suggestions = Suggestion.objects.filter(submission=submission).order_by("seq")
100 context["suggestions"] = self.suggestions
102 self.existing_reviewers = [review.reviewer for review in self.version.reviews.all()]
103 context["existing_reviewers"] = self.existing_reviewers
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
125 context["form_description"] = [description, action]
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
135 return context
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
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 = ""
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))
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"]
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)
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()
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()
192 old_suggested_user.delete()
194 # We now know the reviewer. Set the form.instance so that Django can create a Review
195 form.instance.reviewer = reviewer
197 response = super().form_valid(form)
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 )
207 messages.success(
208 self.request,
209 _(f"Referee request successfully submitted to {review.reviewer}"),
210 )
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 # )
220 return response
222 def get_success_url(self) -> str:
223 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})
226class ReviewEditBaseView(RoleMixin, UpdateView):
227 """
228 Base view for a reviewer to edit a review.
229 """
231 model = Review
232 template_name = "mesh/forms/form_full_page.html"
233 review: Review
234 restricted_roles = [Reviewer]
236 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
237 pk = kwargs["pk"]
238 self.review = get_object_or_404(Review, pk=pk)
240 return not self.role_handler.check_rights("can_edit_review", self.review)
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
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 }
256class ReviewAcceptView(ReviewEditBaseView):
257 """
258 Preliminary view for a reviewer to accept/decline a requested review.
259 """
261 form_class = ReviewAcceptForm
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
272 def get_context_data(self, *args, **kwargs):
273 context = super().get_context_data(*args, **kwargs)
274 context["page_title"] = "Accept/decline review request"
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]
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
293 return context
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})
299 return reverse_lazy(
300 "mesh:submission_details", kwargs={"pk": self.review.version.submission.pk}
301 )
303 def form_valid(self, form: ReviewAcceptForm) -> HttpResponse:
304 accept_value = form.cleaned_data["accepted"]
305 accept_comment = form.cleaned_data["accept_comment"]
307 self.review.accept(accept_value, accept_comment, user=self.request.user)
309 messages.success(
310 self.request,
311 (
312 _("Review request successfully accepted")
313 if accept_value
314 else _("Review request successfully declined")
315 ),
316 )
318 return HttpResponseRedirect(self.get_success_url())
321class ReviewSubmitView(SubmittableModelFormMixin, ReviewEditBaseView):
322 """
323 View for performing a review on a submission version.
324 """
326 form_class = ReviewSubmitForm
327 add_confirm_message = False
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)
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 )
353 def submit_url(self) -> str:
354 return reverse_lazy("mesh:review_confirm", kwargs={"pk": self.review.pk})
356 def get_context_data(self, *args, **kwargs):
357 context = super().get_context_data(*args, **kwargs)
358 context["page_title"] = "Submit review"
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]
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
377 return context
379 def form_pre_save(self, form: ReviewSubmitForm) -> None:
380 form.instance._user = self.request.user
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)
388 if deletion_requested:
389 self.review = get_object_or_404(Review, pk=self.review.pk)
390 return self.get(request, *args, **kwargs)
392 return super().post(request, *args, **kwargs)
395class ReviewConfirmView(RoleMixin, FormView):
396 """
397 View to confirm the submission of a review.
398 """
400 review: Review
401 template_name = "mesh/review/review_confirm.html"
402 form_class = ReviewConfirmForm
404 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs):
405 pk = kwargs["pk"]
406 self.review = get_object_or_404(Review, pk=pk)
408 return not self.role_handler.check_rights("can_submit_review", self.review)
410 def get_success_url(self):
411 return reverse_lazy(
412 "mesh:submission_details", kwargs={"pk": self.review.version.submission.pk}
413 )
415 def get_context_data(self, *args, **kwargs):
416 context = super().get_context_data(*args, **kwargs)
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 ]
427 context["review_proxy"] = ReviewProxy(self.review, self.role_handler)
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
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
442 return context
444 def form_valid(self, form: ReviewConfirmForm) -> HttpResponse:
445 self.review.submit(self.request.user)
447 messages.success(self.request, _("Your review has been successfully submitted."))
449 return HttpResponseRedirect(self.get_success_url())
452REVIEW_FILE_INPUT_PREFIX = "review_file_"
453REVIEW_FILE_INPUT_VALUE = "true"
456class ReviewDetails(RoleMixin, TemplateView):
457 """
458 Details view for a review.
459 """
461 template_name = "mesh/review/review_details.html"
462 review: Review
464 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
465 self.review = get_object_or_404(Review, pk=kwargs["pk"])
467 return not self.role_handler.check_rights("can_access_review", self.review)
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
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
484class ReviewFileAccessUpdate(RoleMixin, View):
485 """
486 View to edit the author access to a review's files.
487 """
489 review: Review
490 form_cache: str
492 def restrict_dispatch(self, request: HttpRequest, *args, **kwargs) -> bool:
493 self.review = get_object_or_404(Review, pk=kwargs["pk"])
495 return not self.role_handler.check_rights("can_edit_review_file_right", self.review)
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
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)
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)
538 messages.success(request, _("Successfully updated the author access right."))
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 )
552 return response
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()
561 return JsonResponse({})
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 """
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]
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)
582 return not self.role_handler.check_rights("can_invite_reviewer", self.version)
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()]
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 )
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 ]
609 def get(self, request, *args, **kwargs):
610 self.init_reviewers()
611 return super().get(self, request, *args, **kwargs)
613 def post(self, request, *args, **kwargs):
614 self.init_reviewers()
615 return super().post(self, request, *args, **kwargs)
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 }
625 def get_context_data(self, *args, **kwargs):
626 context = super().get_context_data(*args, **kwargs)
627 context["page_title"] = "Request review"
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>."
635 context["submission"] = submission
637 # Reviewers already assigned to this round
638 context["existing_reviewers"] = self.existing_reviewers
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
645 context["form_description"] = [description]
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
655 return context
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
664 if len(self.previous_reviewers) > 0:
665 today = datetime.date.today()
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)
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 )
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()
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 )
700 messages.success(
701 self.request,
702 _("Referee request successfully submitted"),
703 )
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 # )
713 else:
714 response = super().form_invalid(form)
716 return response
718 def get_success_url(self) -> str:
719 return reverse_lazy("mesh:submission_details", kwargs={"pk": self.version.submission.pk})