__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.urls import reverse
from django.utils.functional import cached_property
from scipost.storage import SecureFileStorage
from comments.behaviors import validate_file_extension, validate_max_file_size
from journals.models import Publication
from ..behaviors import SubmissionRelatedObjectMixin
from ..constants import (
REPORT_TYPES,
REPORT_NORMAL,
REPORT_STATUSES,
STATUS_DRAFT,
STATUS_UNVETTED,
STATUS_VETTED,
STATUS_INCORRECT,
STATUS_UNCLEAR,
STATUS_NOT_USEFUL,
STATUS_NOT_ACADEMIC,
REFEREE_QUALIFICATION,
RANKING_CHOICES,
QUALITY_SPEC,
REPORT_REC,
)
from ..managers import ReportQuerySet
[docs]class Report(SubmissionRelatedObjectMixin, models.Model):
"""Report on a Submission, written by a Contributor."""
status = models.CharField(
max_length=16, choices=REPORT_STATUSES, default=STATUS_UNVETTED
)
report_type = models.CharField(
max_length=32, choices=REPORT_TYPES, default=REPORT_NORMAL
)
submission = models.ForeignKey(
"submissions.Submission", related_name="reports", on_delete=models.CASCADE
)
report_nr = models.PositiveSmallIntegerField(
default=0,
help_text="This number is a unique number "
"refeering to the Report nr. of "
"the Submission",
)
vetted_by = models.ForeignKey(
"scipost.Contributor",
related_name="report_vetted_by",
blank=True,
null=True,
on_delete=models.CASCADE,
)
# `invited' filled from RefereeInvitation objects at moment of report submission
invited = models.BooleanField(default=False)
# `flagged' if author of report has been flagged by submission authors (surname check only)
flagged = models.BooleanField(default=False)
author = models.ForeignKey(
"scipost.Contributor", on_delete=models.CASCADE, related_name="reports"
)
qualification = models.PositiveSmallIntegerField(
null=True,
blank=True,
choices=REFEREE_QUALIFICATION,
verbose_name="Qualification to referee this: I am",
)
# Text-based reporting
strengths = models.TextField(blank=True)
weaknesses = models.TextField(blank=True)
report = models.TextField(blank=True)
requested_changes = models.TextField(verbose_name="requested changes", blank=True)
# Comments can be added to a Submission
comments = GenericRelation("comments.Comment", related_query_name="reports")
# Qualities:
validity = models.PositiveSmallIntegerField(
choices=RANKING_CHOICES, null=True, blank=True
)
significance = models.PositiveSmallIntegerField(
choices=RANKING_CHOICES, null=True, blank=True
)
originality = models.PositiveSmallIntegerField(
choices=RANKING_CHOICES, null=True, blank=True
)
clarity = models.PositiveSmallIntegerField(
choices=RANKING_CHOICES, null=True, blank=True
)
formatting = models.SmallIntegerField(
choices=QUALITY_SPEC,
null=True,
blank=True,
verbose_name="Quality of paper formatting",
)
grammar = models.SmallIntegerField(
choices=QUALITY_SPEC,
null=True,
blank=True,
verbose_name="Quality of English grammar",
)
recommendation = models.SmallIntegerField(null=True, blank=True, choices=REPORT_REC)
remarks_for_editors = models.TextField(
blank=True, verbose_name="optional remarks for the Editors only"
)
needs_doi = models.BooleanField(null=True, default=None)
doideposit_needs_updating = models.BooleanField(default=False)
genericdoideposit = GenericRelation(
"journals.GenericDOIDeposit", related_query_name="genericdoideposit"
)
doi_label = models.CharField(max_length=200, blank=True)
anonymous = models.BooleanField(default=True, verbose_name="Publish anonymously")
pdf_report = models.FileField(
upload_to="UPLOADS/REPORTS/%Y/%m/", max_length=200, blank=True
)
date_submitted = models.DateTimeField("date submitted")
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
# Attachment
file_attachment = models.FileField(
upload_to="uploads/reports/%Y/%m/%d/",
blank=True,
validators=[validate_file_extension, validate_max_file_size],
storage=SecureFileStorage(),
)
objects = ReportQuerySet.as_manager()
class Meta:
unique_together = ("submission", "report_nr")
default_related_name = "reports"
ordering = ["-date_submitted"]
def __str__(self):
"""Summarize the RefereeInvitation's basic information."""
text = "Anonymous"
if not self.anonymous:
text = self.author.user.first_name + " " + self.author.user.last_name
return (
text
+ " on "
+ self.submission.title[:50]
+ " by "
+ self.submission.author_list[:50]
)
[docs] def save(self, *args, **kwargs):
"""Update report number before saving on creation."""
if not self.report_nr:
new_report_nr = self.submission.reports.aggregate(
models.Max("report_nr")
).get("report_nr__max")
if new_report_nr:
new_report_nr += 1
else:
new_report_nr = 1
self.report_nr = new_report_nr
return super().save(*args, **kwargs)
[docs] def get_absolute_url(self):
"""Return url of the Report on the Submission detail page."""
return self.submission.get_absolute_url() + "#report_" + str(self.report_nr)
[docs] def get_notification_url(self, url_code):
"""Return url related to the Report by the `url_code` meant for Notifications."""
if url_code == "report_form":
return reverse(
"submissions:submit_report",
args=(self.submission.preprint.identifier_w_vn_nr,),
)
elif url_code == "editorial_page":
return reverse(
"submissions:editorial_page",
args=(self.submission.preprint.identifier_w_vn_nr,),
)
return self.get_absolute_url()
[docs] def get_attachment_url(self):
"""Return url of the Report its attachment if exists."""
return reverse(
"submissions:report_attachment",
kwargs={
"identifier_w_vn_nr": self.submission.preprint.identifier_w_vn_nr,
"report_nr": self.report_nr,
},
)
@property
def is_in_draft(self):
"""Return if Report is in draft."""
return self.status == STATUS_DRAFT
@property
def is_vetted(self):
"""Return if Report is publicly available."""
return self.status == STATUS_VETTED
@property
def is_unvetted(self):
"""Return if Report is awaiting vetting."""
return self.status == STATUS_UNVETTED
@property
def is_rejected(self):
"""Return if Report is rejected."""
return self.status in [
STATUS_INCORRECT,
STATUS_UNCLEAR,
STATUS_NOT_USEFUL,
STATUS_NOT_ACADEMIC,
]
@property
def notification_name(self):
"""Return string representation of this Report as shown in Notifications."""
return self.submission.preprint.identifier_w_vn_nr
@property
def doi_string(self):
"""Return the doi with the registrant identifier prefix."""
if self.doi_label:
return "10.21468/" + self.doi_label
return ""
@cached_property
def title(self):
"""Return the submission's title.
This property is (mainly) used to let Comments get the title of the Submission without
overcomplicated logic.
"""
return self.submission.title
@property
def is_followup_report(self):
"""Return if Report is a follow-up Report instead of a regular Report.
This property is used in the ReportForm, but will be candidate to become a database
field if this information becomes necessary in more general information representation.
"""
return (
self.author.reports.accepted()
.filter(
submission__thread_hash=self.submission.thread_hash,
submission__submission_date__lt=self.submission.submission_date,
)
.exists()
)
@property
def associated_published_doi(self):
"""Return the related Publication doi.
Check if the Report relates to a SciPost-published object. If it does, return the doi
of the published object.
"""
try:
publication = Publication.objects.get(
accepted_submission__thread_hash=self.submission.thread_hash
)
except Publication.DoesNotExist:
return None
return publication.doi_string
@property
def relation_to_published(self):
"""Return dictionary with published object information.
Check if the Report relates to a SciPost-published object. If it does, return a dict with
info on relation to the published object, based on Crossref's peer review content type.
"""
try:
publication = Publication.objects.get(
accepted_submission__thread_hash=self.submission.thread_hash
)
except Publication.DoesNotExist:
return None
relation = {
"isReviewOfDOI": publication.doi_string,
"stage": "pre-publication",
"type": "referee-report",
"title": "Report on " + self.submission.preprint.identifier_w_vn_nr,
"contributor_role": "reviewer",
}
return relation
@property
def citation(self):
"""Return the proper citation format for this Report."""
citation = ""
if self.doi_string:
if self.anonymous:
citation += "Anonymous, "
else:
citation += "%s %s, " % (
self.author.user.first_name,
self.author.user.last_name,
)
citation += (
"Report on arXiv:%s, " % self.submission.preprint.identifier_w_vn_nr
)
citation += "delivered %s, " % self.date_submitted.strftime("%Y-%m-%d")
citation += "doi: %s" % self.doi_string
return citation
[docs] def create_doi_label(self):
"""Create a doi in the default format."""
Report.objects.filter(id=self.id).update(
doi_label="SciPost.Report.{}".format(self.id)
)
[docs] def latest_report_from_thread(self):
"""Get latest Report of this Report's author for the Submission thread."""
return (
self.author.reports.accepted()
.filter(submission__thread_hash=self.submission.thread_hash)
.order_by("submission__submission_date")
.last()
)