__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.NullBooleanField(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."""
return (self.author.user.first_name + ' ' + self.author.user.last_name + ' 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__preprint__identifier_wo_vn_nr=self.submission.preprint.identifier_wo_vn_nr,
submission__preprint__vn_nr__lt=self.submission.preprint.vn_nr).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__preprint__identifier_wo_vn_nr=self.submission.preprint.identifier_wo_vn_nr)
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__preprint__identifier_wo_vn_nr=self.submission.preprint.identifier_wo_vn_nr)
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__preprint__identifier_wo_vn_nr=self.submission.preprint.identifier_wo_vn_nr
).order_by('submission__preprint__identifier_wo_vn_nr').last()