__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Avg, F
from django.utils import timezone
from django.urls import reverse
from proceedings.models import Proceedings
from ..constants import ISSUES_ONLY, ISSUE_STATUSES, STATUS_DRAFT, STATUS_PUBLISHED,\
STATUS_PUBLICLY_OPEN
from ..managers import IssueQuerySet
from ..validators import doi_issue_validator
[docs]class Issue(models.Model):
"""
An Issue is related to a specific Journal, either indirectly via a Volume
container, or directly. It is a container for multiple Publications.
"""
in_journal = models.ForeignKey(
'journals.Journal', on_delete=models.CASCADE, null=True, blank=True,
limit_choices_to={'structure': ISSUES_ONLY},
help_text='Assign either a Volume or Journal to the Issue')
in_volume = models.ForeignKey(
'journals.Volume', on_delete=models.CASCADE, null=True, blank=True,
help_text='Assign either a Volume or Journal to the Issue')
number = models.PositiveIntegerField()
slug = models.SlugField()
start_date = models.DateField(default=timezone.now)
until_date = models.DateField(default=timezone.now)
status = models.CharField(max_length=20, choices=ISSUE_STATUSES, default=STATUS_PUBLISHED)
doi_label = models.CharField(max_length=200, unique=True, db_index=True,
validators=[doi_issue_validator])
# absolute path on filesystem: (JOURNALS_DIR)/journal/vol/issue/
path = models.CharField(max_length=200)
objects = IssueQuerySet.as_manager()
class Meta:
default_related_name = 'issues'
ordering = ('-until_date',)
unique_together = ('number', 'in_volume')
def __str__(self):
text = self.issue_string
if hasattr(self, 'proceedings'):
return text
text += ' (%s)' % self.period_as_string
if self.status == STATUS_DRAFT:
text += ' (In draft)'
return text
[docs] def clean(self):
"""Check if either a Journal or Volume is assigned to the Issue."""
if not (self.in_journal or self.in_volume):
raise ValidationError({
'in_journal': ValidationError('Either assign a Journal or Volume to this Issue',
code='required'),
'in_volume': ValidationError('Either assign a Journal or Volume to this Issue',
code='required'),
})
if self.in_journal and not self.in_journal.has_issues:
raise ValidationError({
'in_journal': ValidationError('This journal does not allow for the use of Issues',
code='invalid'),
})
[docs] def get_absolute_url(self):
return reverse('scipost:issue_detail', args=[self.doi_label])
@property
def doi_string(self):
return '10.21468/' + self.doi_label
@property
def issue_string(self):
if self.in_volume:
return '%s issue %s' % (self.in_volume, self.number)
elif self.status == STATUS_PUBLICLY_OPEN:
try:
return '%s (open): %s (%s)' % (self.in_journal,
self.proceedings.event_name, self.number)
except Proceedings.DoesNotExist:
pass
return '%s (open): %s' % (self.in_journal, self.number)
return '%s issue %s' % (self.in_journal, self.number)
@property
def short_str(self):
if self.in_volume:
return 'Vol. %s issue %s' % (self.in_volume.number, self.number)
return 'Issue %s' % self.doi_label.rpartition('.')[2]
@property
def period_as_string(self):
if self.start_date.month == self.until_date.month:
return '%s %s' % (self.until_date.strftime('%B'), self.until_date.strftime('%Y'))
return '%s - %s' % (self.start_date.strftime('%B'), self.until_date.strftime('%B %Y'))
[docs] def get_journal(self):
if self.in_journal:
return self.in_journal
return self.in_volume.in_journal
[docs] def is_current(self):
today = timezone.now().date()
return self.start_date <= today and self.until_date >= today
[docs] def nr_publications(self, tier=None):
from journals.models import Publication
publications = Publication.objects.filter(in_issue=self)
if tier:
publications = publications.filter(
accepted_submission__eicrecommendations__recommendation=tier)
return publications.count()
[docs] def avg_processing_duration(self):
from journals.models import Publication
duration = Publication.objects.filter(
in_issue=self).aggregate(
avg=Avg(F('publication_date') - F('submission_date')))['avg']
if duration:
return duration.total_seconds() / 86400
return 0
[docs] def citation_rate(self, tier=None):
"""Return the citation rate in units of nr citations per article per year."""
from journals.models import Publication
publications = Publication.objects.filter(in_issue=self)
if tier:
publications = publications.filter(
accepted_submission__eicrecommendations__recommendation=tier)
ncites = 0
deltat = 1 # to avoid division by zero
for pub in publications:
if pub.citedby and pub.latest_citedby_update:
ncites += len(pub.citedby)
deltat += (pub.latest_citedby_update.date() - pub.publication_date).days
return (ncites * 365.25 / deltat)