__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import datetime
from django.db import models
from django.db.models import Avg, F
from django.urls import reverse
from django.utils import timezone
from series.models import Collection
from ..constants import JOURNAL_STRUCTURE, ISSUES_AND_VOLUMES, ISSUES_ONLY
from ..managers import JournalQuerySet
from ..validators import doi_journal_validator
[docs]def cost_default_value():
return {"default": 400}
[docs]class Journal(models.Model):
"""Journal is a container of Publications, with a unique issn and doi_label.
Publications may be categorized into issues or issues and volumes.
Each Journal falls under the auspices of a specific College, which is ForeignKeyed.
The only exception is Selections, which does not point to any College
(in fact: it falls under the auspices of all colleges at the same time).
A Journal's AcademicField is indirectly specified via the College, since
College has a ForeignKey to AcademicField.
Specialties can optionally be specified (and should be consistent with the
College's `acad_field`). If none are given, the Journal operates field-wide.
"""
college = models.ForeignKey(
"colleges.College", on_delete=models.PROTECT, related_name="journals"
)
specialties = models.ManyToManyField(
"ontology.Specialty", blank=True, related_name="journals"
)
name = models.CharField(max_length=256, unique=True)
name_abbrev = models.CharField(
max_length=128,
default="SciPost [abbrev]",
help_text="Abbreviated name (for use in citations)",
)
doi_label = models.CharField(
max_length=200, unique=True, db_index=True, validators=[doi_journal_validator]
)
issn = models.CharField(max_length=16, default="2542-4653", blank=True)
active = models.BooleanField(default=True)
submission_allowed = models.BooleanField(default=True)
structure = models.CharField(
max_length=2, choices=JOURNAL_STRUCTURE, default=ISSUES_AND_VOLUMES
)
refereeing_period = models.DurationField(default=datetime.timedelta(days=28))
style = models.TextField(
blank=True,
null=True,
help_text=(
"CSS styling for the journal; the Journal's DOI " "should be used as class"
),
)
# For Journals list page
oneliner = models.TextField(
blank=True,
help_text="One-line description, for Journal card. You can use markup",
)
blurb = models.TextField(default="[To be filled in; you can use markup]")
list_order = models.PositiveSmallIntegerField(default=100)
# For manuscript preparation: templates are given by the SubmissionTemplate related objects
# For the author guidelines page:
required_article_elements = models.TextField(
default="[To be filled in; you can use markup]"
)
# For about page:
description = models.TextField(default="[To be filled in; you can use markup]")
scope = models.TextField(default="[To be filled in; you can use markup]")
content = models.TextField(default="[To be filled in; you can use markup]")
acceptance_criteria = models.TextField(
default="[To be filled in; you can use markup]"
)
submission_insert = models.TextField(
blank=True, null=True, default="[Optional; you can use markup]"
)
minimal_nr_of_reports = models.PositiveSmallIntegerField(
help_text=(
"Minimal number of substantial Reports required "
"before an acceptance motion can be formulated"
),
default=1,
)
has_DOAJ_Seal = models.BooleanField(default=False)
# Templates
template_latex_tgz = models.FileField(
verbose_name="Template (LaTeX, gzipped tarball)",
help_text="Gzipped tarball of the LaTeX template package",
upload_to="UPLOADS/TEMPLATES/latex/%Y/",
max_length=256,
blank=True,
)
template_docx = models.FileField(
verbose_name="Template (.docx)",
help_text=".docx template",
upload_to="UPLOADS/TEMPLATES/docx/%Y/",
max_length=256,
blank=True,
)
# Cost per publication information
cost_info = models.JSONField(default=cost_default_value)
# Calculated fields (to save CPU; field name always starts with cf_)
cf_metrics = models.JSONField(default=dict)
objects = JournalQuerySet.as_manager()
class Meta:
ordering = ["college__acad_field", "list_order"]
def __str__(self):
return self.name
[docs] def get_absolute_url(self):
"""Return Journal's homepage url."""
return reverse("scipost:landing_page", args=(self.doi_label,))
@property
def doi_string(self):
"""Return DOI including the SciPost registrant prefix."""
return "10.21468/" + self.doi_label
@property
def has_issues(self):
return self.structure in (ISSUES_AND_VOLUMES, ISSUES_ONLY)
@property
def has_volumes(self):
return self.structure in (ISSUES_AND_VOLUMES)
@property
def has_collections(self):
return Collection.objects.filter(series__container_journals=self).exists()
[docs] def get_issues(self):
from journals.models import Issue
if self.structure == ISSUES_AND_VOLUMES:
return Issue.objects.filter(in_volume__in_journal=self).published()
elif self.structure == ISSUES_ONLY:
return self.issues.open_or_published()
return Issue.objects.none()
[docs] def get_latest_issue(self):
"""Get latest existing Issue in database irrespective of its status."""
from journals.models import Issue
if self.structure == ISSUES_ONLY:
return self.issues.order_by("-until_date").first()
if self.structure == ISSUES_AND_VOLUMES:
return (
Issue.objects.filter(in_volume__in_journal=self)
.order_by("-until_date")
.first()
)
return None
[docs] def get_latest_volume(self):
"""Get latest existing Volume in database irrespective of its status."""
if self.structure == ISSUES_AND_VOLUMES:
return self.volumes.order_by("-until_date").first()
return None
[docs] def get_publications(self):
from journals.models import Publication
if self.structure == ISSUES_AND_VOLUMES:
return Publication.objects.filter(in_issue__in_volume__in_journal=self)
elif self.structure == ISSUES_ONLY:
return Publication.objects.filter(in_issue__in_journal=self)
return self.publications.all()
[docs] def nr_publications(self, tier=None, year=None):
from journals.models import Publication
publications = self.get_publications()
if year:
publications = publications.filter(publication_date__year=year)
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__in_volume__in_journal=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."""
publications = self.get_publications()
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
[docs] def nr_citations(self, year, specialty=None):
publications = self.get_publications()
if specialty:
publications = publications.filter(specialties=specialty)
ncites = 0
for pub in publications:
if pub.citedby and pub.latest_citedby_update:
for citation in pub.citedby:
if citation["year"] == str(year):
ncites += 1
return ncites
[docs] def citedby_impact_factor(self, year, specialty=None):
"""Compute the impact factor for a given year YYYY, from Crossref cited-by data.
This is defined as the total number of citations in year YYYY
for all papers published in years YYYY-1 and YYYY-2, divided
by the number of papers published in year YYYY.
"""
publications = self.get_publications().filter(
models.Q(publication_date__year=int(year) - 1)
| models.Q(publication_date__year=int(year) - 2)
)
if specialty:
publications = publications.filter(specialties=specialty)
nrpub = publications.count()
if nrpub == 0:
return 0
ncites = 0
for pub in publications:
if pub.citedby and pub.latest_citedby_update:
for citation in pub.citedby:
if citation["year"] == str(year):
ncites += 1
return ncites / nrpub
[docs] def citedby_citescore(self, year, specialty=None):
"""Compute the CiteScore for a given year YYYY, from Crossref cited-by data.
This is defined as the total number of citations in years YYYY to YYYY-3
for all papers published in years YYYY to YYYY-3, divided
by the number of papers published in that same set of years.
"""
publications = self.get_publications().filter(
publication_date__year__lte=int(year),
publication_date__year__gte=int(year) - 3,
)
if specialty:
publications = publications.filter(specialties=specialty)
nrpub = publications.count()
if nrpub == 0:
return 0
ncites = 0
for pub in publications:
if pub.citedby and pub.latest_citedby_update:
for citation in pub.citedby:
if (
int(citation["year"]) <= year
and int(citation["year"]) >= year - 3
):
ncites += 1
return ncites / nrpub
[docs] def cost_per_publication(self, year):
try:
return int(self.cost_info[str(year)])
except KeyError:
return int(self.cost_info["default"])
[docs] def update_cf_metrics(self):
"""
Update the `cf_metrics` calculated field for this Journal.
"""
publications = self.get_publications()
from submissions.models import Submission
if publications:
pubyears = [
year
for year in range(
publications.last().publication_date.year, timezone.now().year
)
]
else:
pubyears = [timezone.now().year]
from submissions.models import Submission
submissions = Submission.objects.filter(
submitted_to=self, is_resubmission_of__isnull=True
)
self.cf_metrics["nr_publications"] = {
"title": "Number of publications per year",
"years": pubyears,
"nr_publications": {
"all": [
publications.filter(publication_date__year=year).count()
for year in pubyears
]
},
}
self.cf_metrics["nr_submissions"] = {
"title": "Number of submissions per year",
"years": pubyears,
"nr_submissions": {
"all": [
submissions.filter(submission_date__year=year).count()
for year in pubyears
]
},
}
self.cf_metrics["nr_citations"] = {
"title": "Number of citations per year",
"years": pubyears,
"nr_citations": {"all": [self.nr_citations(year) for year in pubyears]},
}
self.cf_metrics["citedby_citescore"] = {
"title": "CiteScore",
"years": pubyears,
"citedby_citescore": {
"all": [self.citedby_citescore(year) for year in pubyears]
},
}
self.cf_metrics["citedby_impact_factor"] = {
"title": "Impact Factor",
"years": pubyears,
"citedby_impact_factor": {
"all": [self.citedby_impact_factor(year) for year in pubyears]
},
}
for specialty in self.specialties.all():
self.cf_metrics["nr_publications"]["nr_publications"][specialty.slug] = [
publications.filter(
specialties=specialty, publication_date__year=year
).count()
for year in pubyears
]
self.cf_metrics["nr_submissions"]["nr_submissions"][specialty.slug] = [
submissions.filter(
specialties=specialty, submission_date__year=year
).count()
for year in pubyears
]
self.cf_metrics["nr_citations"]["nr_citations"][specialty.slug] = [
self.nr_citations(year, specialty) for year in pubyears
]
self.cf_metrics["citedby_citescore"]["citedby_citescore"][
specialty.slug
] = [self.citedby_citescore(year, specialty) for year in pubyears]
self.cf_metrics["citedby_impact_factor"]["citedby_impact_factor"][
specialty.slug
] = [self.citedby_impact_factor(year, specialty) for year in pubyears]
self.save()