Source code for submissions.models.report

__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() )