__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(default=timezone.now)
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.NullBooleanField(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 = ['cancelled', 'date_invited']
def __str__(self):
"""Summarize the RefereeInvitation's basic information."""
return (self.first_name + ' ' + self.last_name + ' to referee ' +
self.submission.title[:30] + ' by ' + self.submission.author_list[:30] +
', invited on ' + self.date_invited.strftime('%Y-%m-%d'))
[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_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
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_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