From b885d461552e49b89ca02821c72f360b271511ae Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Mon, 4 Mar 2024 16:14:00 +0100
Subject: [PATCH] make various improvements to referee invitations

fixed #191
---
 scipost_django/profiles/forms.py              |   1 -
 scipost_django/submissions/forms/__init__.py  |  83 +++++++---
 .../_hx_select_referee_table_row.html         |  52 +++---
 .../templates/submissions/select_referee.html |  12 +-
 scipost_django/submissions/urls/__init__.py   |   7 +-
 scipost_django/submissions/views/__init__.py  | 150 +++++++++++++++++-
 6 files changed, 250 insertions(+), 55 deletions(-)

diff --git a/scipost_django/profiles/forms.py b/scipost_django/profiles/forms.py
index a464b79b8..1a461b96e 100644
--- a/scipost_django/profiles/forms.py
+++ b/scipost_django/profiles/forms.py
@@ -48,7 +48,6 @@ class ProfileForm(forms.ModelForm):
 
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
-        self.fields["email"].initial = self.instance.email
         self.fields["instance_from_type"].widget = forms.HiddenInput()
         self.fields["instance_pk"].widget = forms.HiddenInput()
 
diff --git a/scipost_django/submissions/forms/__init__.py b/scipost_django/submissions/forms/__init__.py
index e192349e5..12b8839fc 100644
--- a/scipost_django/submissions/forms/__init__.py
+++ b/scipost_django/submissions/forms/__init__.py
@@ -12,7 +12,15 @@ import datetime
 from django import forms
 from django.conf import settings
 from django.db import transaction
-from django.db.models import Q, Count, Exists, OuterRef, Subquery, Value
+from django.db.models import (
+    Q,
+    Count,
+    Exists,
+    OuterRef,
+    Value,
+    BooleanField,
+    ExpressionWrapper,
+)
 from django.db.models.functions import Concat
 from django.shortcuts import get_object_or_404
 from django.forms.formsets import ORDERING_FIELD_NAME
@@ -2255,10 +2263,12 @@ class InviteRefereeSearchFrom(forms.Form):
 
     show_unavailable = forms.BooleanField(
         required=False,
+        initial=True,
         label="Show unavailable",
     )
     show_with_CI = forms.BooleanField(
         required=False,
+        initial=True,
         label="Include those with competing interests",
     )
     show_email_unknown = forms.BooleanField(
@@ -2389,6 +2399,34 @@ class InviteRefereeSearchFrom(forms.Form):
         """
         self.save_fields_to_session()
 
+        profiles = (
+            Profile.objects.all()
+            .annot_has_competing_interests_with_submission_authors(self.submission)
+            .annotate(
+                is_unavailable=Exists(
+                    UnavailabilityPeriod.objects.today().filter(
+                        contributor=OuterRef("contributor")
+                    )
+                )
+            )
+            .annotate(
+                last_name_matches=Exists(
+                    Submission.objects.filter(
+                        id=self.submission.id,
+                        author_list__unaccent__icontains=OuterRef("last_name"),
+                    )
+                )
+            )
+            .annotate(
+                has_accepted_previous_invitation=Exists(
+                    RefereeInvitation.objects.filter(
+                        referee=OuterRef("contributor"),
+                        submission__thread_hash=self.submission.thread_hash,
+                        accepted=True,
+                    ).exclude(submission=self.submission)
+                )
+            )
+        )
 
         if text := self.cleaned_data.get("text"):
             profiles = profiles.search(text)
@@ -2400,22 +2438,10 @@ class InviteRefereeSearchFrom(forms.Form):
 
         # Filter to only those without competing interests, unless the option is selected
         if not self.cleaned_data.get("show_with_CI"):
-            profiles = (
-                profiles.without_competing_interests_against_submission_authors_of(
-                    self.submission
-                )
-            )
+            profiles = profiles.exclude(has_any_competing_interest_with_submission=True)
         # Filter to only those available, unless the option is selected
         if not self.cleaned_data.get("show_unavailable"):
-            current_unavailability_periods = Subquery(
-                UnavailabilityPeriod.objects.today().filter(
-                    contributor=OuterRef("contributor")
-                )
-            )
-            profiles = profiles.annotate(
-                is_unavailable=Exists(current_unavailability_periods)
-            ).exclude(is_unavailable=True)
-
+            profiles = profiles.exclude(is_unavailable=True)
         # Exclude those without email, if the option is selected
         if not self.cleaned_data.get("show_email_unknown"):
             profiles = profiles.exclude(emails__isnull=True)
@@ -2440,6 +2466,21 @@ class InviteRefereeSearchFrom(forms.Form):
                 ]
             )
 
+        profiles = profiles.annotate(
+            can_be_sent_invitation=ExpressionWrapper(
+                Q(emails__isnull=False)
+                & Q(accepts_refereeing_requests=True)
+                & ~Q(has_any_competing_interest_with_submission=True)
+                & (Q(is_unavailable=False) | Q(has_accepted_previous_invitation=True)),
+                output_field=BooleanField(),
+            ),
+            warned_against_invitation=ExpressionWrapper(
+                (Q(is_submission_author=False) & Q(last_name_matches=True))
+                | (Q(is_unavailable=True) & Q(has_accepted_previous_invitation=True)),
+                output_field=BooleanField(),
+            ),
+        )
+
         return profiles
 
 
@@ -2452,7 +2493,7 @@ class ConfigureRefereeInvitationForm(forms.Form):
     has_auto_reminders = forms.ChoiceField(
         widget=forms.RadioSelect,
         choices=((True, "Yes"), (False, "No")),
-        initial=False,
+        initial=True,
         required=False,
         label="Send automatic reminders?",
     )
@@ -2510,16 +2551,6 @@ class ConfigureRefereeInvitationForm(forms.Form):
             )
         )
 
-    def clean(self):
-        if (
-            contributor := getattr(self.profile, "contributor", None)
-        ) and not contributor.is_currently_available:
-            self.add_error(
-                None,
-                "This Contributor is marked as currently unavailable. "
-                "Please cancel and select another referee.",
-            )
-
 
 class ConsiderRefereeInvitationForm(forms.Form):
     accept = forms.ChoiceField(
diff --git a/scipost_django/submissions/templates/submissions/_hx_select_referee_table_row.html b/scipost_django/submissions/templates/submissions/_hx_select_referee_table_row.html
index e55ec8164..2f6d097ce 100644
--- a/scipost_django/submissions/templates/submissions/_hx_select_referee_table_row.html
+++ b/scipost_django/submissions/templates/submissions/_hx_select_referee_table_row.html
@@ -38,31 +38,47 @@
 
   </td>
   <td style="min-width:330px;">
+    <div class="d-flex">
+      <button id="ref-inv-{{ profile.id }}-send-btn"
+              type="button"
+              class="btn btn-sm btn-light me-2"
+              hx-get="{% url 'submissions:_hx_add_referee_profile_email' profile_id=profile.id %}"
+              hx-target="closest tr"
+              hx-swap="afterend">Add Email</button>
+
+      <button id="ref-inv-{{ profile.id }}-send-btn" type="button" class="me-2 btn btn-sm btn-{% if profile.warned_against_invitation %}warning{% else %}light{% endif %}" 
+      {% if profile.warned_against_invitation %}hx-confirm="Do you want to send an invitation to this referee despite the warning?"{% endif %}
+      hx-get="{% url 'submissions:_hx_configure_refereeing_invitation' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id %}" hx-target="closest tr" 
+      {% if not profile.can_be_sent_invitation %}disabled{% endif %}
+      hx-swap="afterend">Configure</button>
+      <button id="ref-inv-{{ profile.id }}-send-btn" type="button" class="ms-auto btn btn-sm btn-{% if profile.warned_against_invitation %}warning{% else %}primary{% endif %}" 
+      {% if profile.warned_against_invitation %}hx-confirm="Do you want to send an invitation to this referee despite the warning?"{% endif %}
+      hx-get="{% url 'submissions:_hx_quick_invite_referee' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id %}" hx-target="closest td" 
+      {% if not profile.can_be_sent_invitation %}disabled{% endif %}
+      hx-swap="outerHTML">Quick Send</button>
+    </div>
 
     {% if not profile.accepts_refereeing_requests %}
-      <span class="text-danger">This person does not accept refereeing requests</span>
-    {% elif not profile.emails.all %}
-      <div class="text-danger">Cannot send an invitation without an email.</div>
-    {% elif profile.contributor and not profile.contributor.is_currently_available %}
-      <div class="text-danger">
+      <div class="text-danger">This person does not accept refereeing requests</div>
+    {% endif %}
+
+    {% if profile.has_submission_competing_interests %}
+      <div class="text-danger">This person has a competing interest with the submission</div>
+    {% endif %}
+
+    {% if profile.is_submission_author %}
+      <div class="text-danger">This person is an author of the submission</div>
+    {% elif profile.last_name_matches %}
+      <div class="text-warning">This person could be an author of the submission (last name matches)</div>
+    {% endif %}
+
+    {% if profile.contributor and not profile.contributor.is_currently_available %}
+      <div class="text-warning">
         This person is not currently available, but will be after
         <time datetime="{{ profile.contributor.available_again_after_date }}">
           {{ profile.contributor.available_again_after_date }}
         </time>
       </div>
-    {% else %}
-      <button id="ref-inv-{{ profile.id }}-send-btn"
-              type="button"
-              class="btn btn-sm btn-light"
-              hx-get="{% url 'submissions:_hx_configure_refereeing_invitation' identifier_w_vn_nr=submission.preprint.identifier_w_vn_nr profile_id=profile.id %}"
-              hx-target="closest tr"
-              hx-swap="afterend">Select</button>
-      <button id="ref-inv-{{ profile.id }}-send-btn"
-              type="button"
-              class="btn btn-sm btn-light"
-              hx-get="{% url 'submissions:_hx_add_referee_profile_email' profile_id=profile.id %}"
-              hx-target="closest tr"
-              hx-swap="afterend">Add Email</button>
     {% endif %}
 
   </td>
diff --git a/scipost_django/submissions/templates/submissions/select_referee.html b/scipost_django/submissions/templates/submissions/select_referee.html
index 178cd4af6..d08b075a0 100644
--- a/scipost_django/submissions/templates/submissions/select_referee.html
+++ b/scipost_django/submissions/templates/submissions/select_referee.html
@@ -45,7 +45,17 @@
     </div>
   </div>
 
-  <h2 class="highlight">Invite an additional Referee</h2>
+  <h2 class="highlight">Invite a Referee</h2>
+
+  <p>
+    You can invite a referee to review this submission by selecting a referee from the list below. If the referee is not in the list, you can add them to our database by filling the form at the bottom of this page.
+    There are three actions you can perform on a referee:
+    <ul>
+      <li><strong>Add</strong> an <strong>Email</strong> to the referee's profile.</li>
+      <li><strong>Configure</strong> the invitation by selecting an alternative email address, whether auto-reminders are sent, and the exact email content.</li>
+      <li><strong>Quick Send</strong> an invitation to the primary email, with auto-reminders, and the default email content.</li>
+    </ul>
+  </p>
 
   {% if workdays_left_to_report < 15 %}
     <div class="mb-3 p-3 border border-danger border-2">
diff --git a/scipost_django/submissions/urls/__init__.py b/scipost_django/submissions/urls/__init__.py
index 41a68e189..78d80a3b9 100644
--- a/scipost_django/submissions/urls/__init__.py
+++ b/scipost_django/submissions/urls/__init__.py
@@ -319,10 +319,15 @@ urlpatterns = [
         name="add_referee_profile",
     ),
     path(
-        "invite_referee/<identifier:identifier_w_vn_nr>/<int:profile_id>/<str:profile_email>/auto_remind/<str:auto_reminders_allowed>",
+        "invite_referee/<identifier:identifier_w_vn_nr>/<int:profile_id>/custom/<str:profile_email>/auto_remind_<str:auto_reminders_allowed>",
         views.invite_referee,
         name="invite_referee",
     ),
+    path(
+        "invite_referee/<identifier:identifier_w_vn_nr>/<int:profile_id>/quick",
+        views._hx_quick_invite_referee,
+        name="_hx_quick_invite_referee",
+    ),
     path(
         "refereeing/_hx_configure_invitation/<identifier:identifier_w_vn_nr>/<int:profile_id>",
         views._hx_configure_refereeing_invitation,
diff --git a/scipost_django/submissions/views/__init__.py b/scipost_django/submissions/views/__init__.py
index e3d1855c6..98ca6a159 100644
--- a/scipost_django/submissions/views/__init__.py
+++ b/scipost_django/submissions/views/__init__.py
@@ -35,7 +35,11 @@ from django.views.generic.list import ListView
 
 from dal import autocomplete
 
-from scipost.permissions import permission_required_htmx
+from scipost.permissions import (
+    HTMXPermissionsDenied,
+    HTMXResponse,
+    permission_required_htmx,
+)
 
 from ..constants import (
     STATUS_VETTED,
@@ -1352,9 +1356,22 @@ def invite_referee(
     profile = get_object_or_404(Profile, pk=profile_id)
     auto_reminders_allowed = auto_reminders_allowed == "True"
 
+    # Guard against profiles who don't want to referee
+    if not profile.accepts_refereeing_requests:
+        messages.error(
+            request,
+            "This person has indicated that they do not want to be invited to referee.",
+        )
+        return redirect(
+            reverse(
+                "submissions:editorial_page",
+                kwargs={"identifier_w_vn_nr": identifier_w_vn_nr},
+            )
+        )
+
     # Guard against profiles with competing interests
     if not (
-        Profile.objects.filter(profile=profile)
+        Profile.objects.filter(pk=profile.pk)
         .without_competing_interests_against_submission_authors_of(submission)
         .exists()
     ):
@@ -1398,13 +1415,22 @@ def invite_referee(
         referee_invitation.save()
 
     registration_invitation = None
+    has_agreed_to_previous_invitation = RefereeInvitation.objects.filter(
+        profile=profile, submission__thread_hash=submission.thread_hash, accepted=True
+    ).exists()
+
     if contributor:
-        if not profile.contributor.is_currently_available:
-            errormessage = (
-                "This Contributor is marked as currently unavailable. "
-                "Please go back and select another referee."
+        if (
+            not contributor.is_currently_available
+            and not has_agreed_to_previous_invitation
+        ):
+            error_message = (
+                "This referee is not currently available, "
+                "and has not accepted a previous invitation for this submission."
+            )
+            return render(
+                request, "scipost/error.html", {"errormessage": error_message}
             )
-            return render(request, "scipost/error.html", {"errormessage": errormessage})
 
         mail_request = MailEditorSubview(
             request,
@@ -1443,7 +1469,6 @@ def invite_referee(
         submission.add_event_for_author("A referee has been invited.")
         submission.add_event_for_eic("Referee %s has been invited." % profile.last_name)
         messages.success(request, "Invitation sent")
-        mail_request.send_mail()
         return redirect(
             reverse(
                 "submissions:editorial_page",
@@ -1454,6 +1479,115 @@ def invite_referee(
         return mail_request.interrupt()
 
 
+@transaction.atomic
+def _hx_quick_invite_referee(request, identifier_w_vn_nr, profile_id):
+    submission = get_object_or_404(
+        Submission, preprint__identifier_w_vn_nr=identifier_w_vn_nr
+    )
+    profile = get_object_or_404(Profile, pk=profile_id)
+
+    # Guard against non-admin and non-EIC users
+    is_eic_for_submission = submission.editor_in_charge == request.user.contributor
+    can_oversee_refereeing = request.user.has_perm("scipost.can_oversee_refereeing")
+    if not (is_eic_for_submission or can_oversee_refereeing):
+        return HTMXPermissionsDenied("You do not have permission to invite referees.")
+
+    # Guard against profiles who don't want to referee
+    if not profile.accepts_refereeing_requests:
+        return HTMXResponse(
+            "This person has indicated that they do not want to be invited to referee.",
+            tag="danger",
+        )
+
+    # Guard against profiles with competing interests
+    if not (
+        Profile.objects.filter(pk=profile.pk)
+        .without_competing_interests_against_submission_authors_of(submission)
+        .exists()
+    ):
+        return HTMXResponse(
+            "This Profile has a competing interest with the authors of the Submission.",
+            tag="danger",
+        )
+
+    contributor = None
+    if hasattr(profile, "contributor") and profile.contributor:
+        contributor = profile.contributor
+
+    referee_invitation, created = RefereeInvitation.objects.get_or_create(
+        profile=profile,
+        referee=contributor,
+        submission=submission,
+        title=profile.title if profile.title else TITLE_DR,
+        first_name=profile.first_name,
+        last_name=profile.last_name,
+        email_address=profile.email,
+        auto_reminders_allowed=True,
+        invited_by=request.user.contributor,
+    )
+
+    key = ""
+    if created:
+        key = get_new_secrets_key()
+        referee_invitation.invitation_key = key
+        referee_invitation.save()
+
+    registration_invitation = None
+    has_agreed_to_previous_invitation = RefereeInvitation.objects.filter(
+        profile=profile, submission__thread_hash=submission.thread_hash, accepted=True
+    ).exists()
+
+    if contributor:
+        if (
+            not contributor.is_currently_available
+            and not has_agreed_to_previous_invitation
+        ):
+            return HTMXResponse(
+                "This referee is not currently available, "
+                "and has not accepted a previous invitation for this submission.",
+                tag="danger",
+            )
+
+        mail_request = DirectMailUtil(
+            "referees/invite_contributor_to_referee",
+            invitation=referee_invitation,
+        )
+    else:  # no Contributor, so registration invitation
+        registration_invitation, reginv_created = (
+            RegistrationInvitation.objects.get_or_create(
+                profile=profile,
+                title=profile.title if profile.title else TITLE_DR,
+                first_name=profile.first_name,
+                last_name=profile.last_name,
+                email=profile.email,
+                invitation_type=INVITATION_REFEREEING,
+                created_by=request.user,
+                invited_by=request.user,
+                invitation_key=referee_invitation.invitation_key,
+            )
+        )
+        mail_request = DirectMailUtil(
+            mail_code="referees/invite_unregistered_to_referee",
+            invitation=referee_invitation,
+        )
+
+    mail_request.send_mail()
+
+    referee_invitation.date_invited = timezone.now()
+    referee_invitation.save()
+    if registration_invitation:
+        registration_invitation.status = STATUS_SENT
+        registration_invitation.key_expires = timezone.now() + datetime.timedelta(
+            days=365
+        )
+        registration_invitation.save()
+
+    submission.add_event_for_author("A referee has been invited.")
+    submission.add_event_for_eic("Referee %s has been invited." % profile.last_name)
+
+    return HTMXResponse("Invitation sent", tag="success")
+
+
 @login_required
 @fellowship_or_admin_required()
 def set_refinv_auto_reminder(request, invitation_id, auto_reminders):
-- 
GitLab