SciPost Code Repository

Skip to content
Snippets Groups Projects
report.py 10.3 KiB
Newer Older
__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


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")
    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"]
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
        """Summarize the RefereeInvitation's basic information."""
Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
        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]
        )

    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)

    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)

    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 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.

Jean-Sébastien Caux's avatar
Jean-Sébastien Caux committed
        Check if the Report relates to a SciPost-published object. If it does, return the doi
        publication = Publication.objects.filter(
            accepted_submission__thread_hash=self.submission.thread_hash
        ).order_by('doi_label').first()
        # order by doi_label to give priority to main article, which has no DOI suffix
        if publication:
            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.
        """
        publication = Publication.objects.filter(
            accepted_submission__thread_hash=self.submission.thread_hash
        ).order_by('doi_label').first()

        if publication:
            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."""
        if self.doi_string:
            if self.anonymous:
                citation += "Anonymous, "
                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

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

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