__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import datetime
from django.db import models
from django.urls import reverse
from django.utils import timezone
from scipost.constants import TITLE_CHOICES
from ..behaviors import SubmissionRelatedObjectMixin
from ..constants import ASSIGNMENT_NULLBOOL, ASSIGNMENT_REFUSAL_REASONS
from ..managers import RefereeInvitationQuerySet
[docs]class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model):
"""Invitation to an active professional scientist to referee a Submission.
A RefereeInvitation represents an invitation to a Contributor
or a non-registered scientist to write a Report for a specific Submission.
The instance will register the response to the invitation and
the current status of the refereeing duty if the invitation has been accepted.
"""
profile = models.ForeignKey(
"profiles.Profile", on_delete=models.SET_NULL, blank=True, null=True
)
submission = models.ForeignKey(
"submissions.Submission",
on_delete=models.CASCADE,
related_name="referee_invitations",
)
referee = models.ForeignKey(
"scipost.Contributor",
related_name="referee_invitations",
blank=True,
null=True,
on_delete=models.CASCADE,
)
title = models.CharField(max_length=4, choices=TITLE_CHOICES)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email_address = models.EmailField()
# if Contributor not found, person is invited to register
invitation_key = models.CharField(max_length=40, blank=True)
date_invited = models.DateTimeField(blank=True, null=True)
invited_by = models.ForeignKey(
"scipost.Contributor",
related_name="referee_invited_by",
blank=True,
null=True,
on_delete=models.CASCADE,
)
auto_reminders_allowed = models.BooleanField(default=True)
nr_reminders = models.PositiveSmallIntegerField(default=0)
date_last_reminded = models.DateTimeField(blank=True, null=True)
accepted = models.BooleanField(
blank=True, null=True, choices=ASSIGNMENT_NULLBOOL, default=None
)
date_responded = models.DateTimeField(blank=True, null=True)
refusal_reason = models.CharField(
max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS, blank=True, null=True
)
fulfilled = models.BooleanField(
default=False
) # True if a Report has been submitted
cancelled = models.BooleanField(
default=False
) # True if EIC has deactivated invitation
objects = RefereeInvitationQuerySet.as_manager()
class Meta:
ordering = [
"-date_invited",
]
def __str__(self):
"""Summarize the RefereeInvitation's basic information."""
value = (
self.first_name
+ " "
+ self.last_name
+ " to referee "
+ self.submission.title[:30]
+ " by "
+ self.submission.author_list[:30]
)
if self.date_invited:
value += ", invited on " + self.date_invited.strftime("%Y-%m-%d")
else:
value += ", NO EMAIL SENT YET"
return value
[docs] def get_absolute_url(self):
"""Return url of the invitation's processing page."""
return reverse("submissions:accept_or_decline_ref_invitations", args=(self.id,))
@property
def referee_str(self):
"""Return the most up-to-date name of the Referee."""
if self.referee:
return str(self.referee)
return self.last_name + ", " + self.first_name
@property
def notification_name(self):
"""Return string representation of this RefereeInvitation as shown in Notifications."""
return self.submission.preprint.identifier_w_vn_nr
@property
def related_report(self):
"""Return the Report that's been created for this invitation."""
return self.submission.reports.filter(author=self.referee).last()
@property
def needs_sending(self):
"""Check if the invitation has been emailed."""
if not self.date_invited:
return True
return False
@property
def needs_response(self):
"""Check if invitation has no response in more than three days."""
if not self.cancelled and self.accepted is None:
if self.date_last_reminded:
# No reponse in over three days since last reminder
return timezone.now() - self.date_last_reminded > datetime.timedelta(
days=3
)
# No reponse in over three days since original invite
if not self.date_invited:
return True
else:
return timezone.now() - self.date_invited > datetime.timedelta(days=3)
return False
@property
def needs_fulfillment_reminder(self):
"""Check if isn't fullfilled but deadline is closing in."""
if self.accepted and not self.cancelled and not self.fulfilled:
# Refereeing deadline closing in/overdue, but invitation isn't fulfilled yet.
return (self.submission.reporting_deadline - timezone.now()).days < 7
return False
@property
def is_overdue(self):
"""Check if isn't fullfilled but deadline has expired."""
if self.accepted and not self.cancelled and not self.fulfilled:
# Refereeing deadline closing in/overdue, but invitation isn't fulfilled yet.
return (self.submission.reporting_deadline - timezone.now()).days < 0
return False
@property
def needs_attention(self):
"""Check if invitation needs attention by the editor."""
return (
self.needs_sending or self.needs_response or self.needs_fulfillment_reminder
)
@property
def get_status_display(self):
"""Get status: a combination between different boolean fields."""
if self.cancelled:
return "Cancelled"
if self.fulfilled:
return "Fulfilled"
if self.accepted is None:
return "Awaiting response"
elif self.accepted:
return "Accepted"
else:
return "Declined ({})".format(self.get_refusal_reason_display())
[docs] def reset_content(self):
"""Reset the invitation's information as a new invitation."""
self.nr_reminders = 0
self.date_last_reminded = None
self.accepted = None
self.refusal_reason = None
self.fulfilled = False
self.cancelled = False