import datetime
from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import GenericRelation
from django.utils import timezone
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.functional import cached_property
from .behaviors import SubmissionRelatedObjectMixin
from .constants import ASSIGNMENT_REFUSAL_REASONS, ASSIGNMENT_NULLBOOL,\
SUBMISSION_TYPE, ED_COMM_CHOICES, REFEREE_QUALIFICATION, QUALITY_SPEC,\
RANKING_CHOICES, REPORT_REC, SUBMISSION_STATUS, STATUS_UNASSIGNED,\
REPORT_STATUSES, STATUS_UNVETTED, SUBMISSION_EIC_RECOMMENDATION_REQUIRED,\
SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC,\
EVENT_GENERAL, EVENT_TYPES, EVENT_FOR_AUTHOR, EVENT_FOR_EIC,\
REPORT_TYPES, REPORT_NORMAL, STATUS_DRAFT, STATUS_VETTED,\
STATUS_VOTING_IN_PREPARATION, STATUS_PUT_TO_EC_VOTING
from .managers import SubmissionQuerySet, EditorialAssignmentQuerySet, EICRecommendationQuerySet,\
ReportQuerySet, SubmissionEventQuerySet, RefereeInvitationQuerySet,\
EditorialCommunicationQueryset
from .utils import ShortSubmissionCycle, DirectRecommendationSubmissionCycle,\
GeneralSubmissionCycle
from comments.models import Comment
from scipost.behaviors import TimeStampedModel
from scipost.constants import TITLE_CHOICES
from scipost.fields import ChoiceArrayField
from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
from journals.constants import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS
from journals.models import Publication
[docs]class Submission(models.Model):
"""
Submission is a SciPost register of an ArXiv article. This object is the central
instance for every action, recommendation, communication, etc. etc. that is related to the
refereeing cycle of a Submission. A possible Publication object is later directly related
to this Submission instance.
"""
author_comments = models.TextField(blank=True)
author_list = models.CharField(max_length=1000, verbose_name="author list")
discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics')
domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS)
editor_in_charge = models.ForeignKey('scipost.Contributor', related_name='EIC', blank=True,
null=True, on_delete=models.CASCADE)
is_current = models.BooleanField(default=True)
is_resubmission = models.BooleanField(default=False)
list_of_changes = models.TextField(blank=True)
open_for_commenting = models.BooleanField(default=False)
open_for_reporting = models.BooleanField(default=False)
referees_flagged = models.TextField(blank=True)
referees_suggested = models.TextField(blank=True)
remarks_for_editors = models.TextField(blank=True)
reporting_deadline = models.DateTimeField(default=timezone.now)
secondary_areas = ChoiceArrayField(
models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS),
blank=True, null=True)
# Refereeing fields
status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default=STATUS_UNASSIGNED)
refereeing_cycle = models.CharField(max_length=30, choices=SUBMISSION_CYCLES,
default=CYCLE_DEFAULT)
fellows = models.ManyToManyField('colleges.Fellowship', blank=True,
related_name='pool')
subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS,
verbose_name='Primary subject area', default='Phys:QP')
submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE,
blank=True, null=True, default=None)
submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE,
related_name='submitted_submissions')
voting_fellows = models.ManyToManyField('colleges.Fellowship', blank=True,
related_name='voting_pool')
# Replace this by foreignkey?
submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT,
verbose_name="Journal to be submitted to")
proceedings = models.ForeignKey('proceedings.Proceedings', null=True, blank=True,
related_name='submissions')
title = models.CharField(max_length=300)
# Authors which have been mapped to contributors:
authors = models.ManyToManyField('scipost.Contributor', blank=True, related_name='submissions')
authors_claims = models.ManyToManyField('scipost.Contributor', blank=True,
related_name='claimed_submissions')
authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True,
related_name='false_claimed_submissions')
abstract = models.TextField()
# Comments can be added to a Submission
comments = GenericRelation('comments.Comment', related_query_name='submissions')
# iThenticate Reports
plagiarism_report = models.OneToOneField('submissions.iThenticateReport',
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='to_submission')
# Arxiv identifiers with/without version number
arxiv_identifier_w_vn_nr = models.CharField(max_length=15, default='0000.00000v0')
arxiv_identifier_wo_vn_nr = models.CharField(max_length=10, default='0000.00000')
arxiv_vn_nr = models.PositiveSmallIntegerField(default=1)
arxiv_link = models.URLField(verbose_name='arXiv link (including version nr)')
pdf_refereeing_pack = models.FileField(upload_to='UPLOADS/REFEREE/%Y/%m/',
max_length=200, blank=True)
# Metadata
metadata = JSONField(default={}, blank=True, null=True)
submission_date = models.DateField(verbose_name='submission date', default=datetime.date.today)
acceptance_date = models.DateField(verbose_name='acceptance date', null=True, blank=True)
latest_activity = models.DateTimeField(auto_now=True)
objects = SubmissionQuerySet.as_manager()
class Meta:
app_label = 'submissions'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._update_cycle()
def save(self, *args, **kwargs):
# Fill `arxiv_identifier_w_vn_nr` as a dummy field for convenience
arxiv_w_vn = '{arxiv}v{version}'.format(
arxiv=self.arxiv_identifier_wo_vn_nr,
version=self.arxiv_vn_nr)
self.arxiv_identifier_w_vn_nr = arxiv_w_vn
obj = super().save(*args, **kwargs)
self._update_cycle()
return obj
def __str__(self):
header = (self.arxiv_identifier_w_vn_nr + ', '
+ self.title[:30] + ' by ' + self.author_list[:30])
if self.is_current:
header += ' (current version)'
else:
header += ' (deprecated version ' + str(self.arxiv_vn_nr) + ')'
try:
header += ' (published as %s (%s))' % (
self.publication.doi_string, self.publication.publication_date.strftime('%Y'))
except Publication.DoesNotExist:
pass
return header
[docs] def touch(self):
""" Update latest activity as a service """
self.latest_activity = timezone.now()
self.save()
def _update_cycle(self):
"""
Append the specific submission cycle to the instance to eventually handle the
complete submission cycle outside the submission instance itself.
"""
if self.refereeing_cycle == CYCLE_SHORT:
self.cycle = ShortSubmissionCycle(self)
elif self.refereeing_cycle == CYCLE_DIRECT_REC:
self.cycle = DirectRecommendationSubmissionCycle(self)
else:
self.cycle = GeneralSubmissionCycle(self)
def get_absolute_url(self):
return reverse('submissions:submission', args=[self.arxiv_identifier_w_vn_nr])
@property
def notification_name(self):
return self.arxiv_identifier_w_vn_nr
@property
def eic_recommendation_required(self):
return self.status in SUBMISSION_EIC_RECOMMENDATION_REQUIRED
@property
def reporting_deadline_has_passed(self):
return timezone.now() > self.reporting_deadline
@property
def original_submission_date(self):
return Submission.objects.filter(
arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr).first().submission_date
@cached_property
def thread(self):
"""
Return all versions of the Submission with that arxiv id.
"""
return Submission.objects.public().filter(
arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr
).order_by('-arxiv_vn_nr')
@cached_property
def other_versions(self):
"""
Return all other versions of the Submission that are publicly accessible.
"""
return Submission.objects.public().filter(
arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr
).exclude(pk=self.id).order_by('-arxiv_vn_nr')
@cached_property
def other_versions_pool(self):
"""
Return all other versions of the Submission.
"""
return Submission.objects.filter(
arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr
).exclude(pk=self.id).order_by('-arxiv_vn_nr')
# Underneath: All very inefficient methods as they initiate a new query
def count_accepted_invitations(self):
return self.referee_invitations.filter(accepted=True).count()
def count_declined_invitations(self):
return self.referee_invitations.filter(accepted=False).count()
def count_pending_invitations(self):
return self.referee_invitations.filter(accepted=None).count()
def count_invited_reports(self):
return self.reports.accepted().filter(invited=True).count()
def count_contrib_reports(self):
return self.reports.accepted().filter(invited=False).count()
def count_obtained_reports(self):
return self.reports.accepted().filter(invited__isnull=False).count()
def add_general_event(self, message):
event = SubmissionEvent(
submission=self,
event=EVENT_GENERAL,
text=message,
)
event.save()
def add_event_for_author(self, message):
event = SubmissionEvent(
submission=self,
event=EVENT_FOR_AUTHOR,
text=message,
)
event.save()
def add_event_for_eic(self, message):
event = SubmissionEvent(
submission=self,
event=EVENT_FOR_EIC,
text=message,
)
event.save()
[docs]class SubmissionEvent(SubmissionRelatedObjectMixin, TimeStampedModel):
"""
The SubmissionEvent's goal is to act as a messaging/logging model
for the Submission cycle. Its main audience will be the author(s) and
the Editor-in-charge of a Submission.
Be aware!
Both the author and editor-in-charge will read the submission event.
Make sure the right text is given to the right event-type, to protect
the fellow's identity.
"""
submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
related_name='events')
event = models.CharField(max_length=4, choices=EVENT_TYPES, default=EVENT_GENERAL)
text = models.TextField()
objects = SubmissionEventQuerySet.as_manager()
class Meta:
ordering = ['-created']
def __str__(self):
return '%s: %s' % (str(self.submission), self.get_event_display())
######################
# Editorial workflow #
######################
[docs]class EditorialAssignment(SubmissionRelatedObjectMixin, models.Model):
"""
EditorialAssignment is a registration for Fellows of their duties of being a
Editor-in-charge for a specific Submission. This model could start as a invitation only,
which should then be accepted or declined by the invited.
"""
submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE)
to = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE)
accepted = models.NullBooleanField(choices=ASSIGNMENT_NULLBOOL, default=None)
# attribute `deprecated' becomes True if another Fellow becomes Editor-in-charge
deprecated = models.BooleanField(default=False)
completed = models.BooleanField(default=False)
refusal_reason = models.CharField(max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS,
blank=True, null=True)
date_created = models.DateTimeField(default=timezone.now)
date_answered = models.DateTimeField(blank=True, null=True)
objects = EditorialAssignmentQuerySet.as_manager()
class Meta:
default_related_name = 'editorial_assignments'
ordering = ['-date_created']
def __str__(self):
return (self.to.user.first_name + ' ' + self.to.user.last_name + ' to become EIC of ' +
self.submission.title[:30] + ' by ' + self.submission.author_list[:30] +
', requested on ' + self.date_created.strftime('%Y-%m-%d'))
def get_absolute_url(self):
return reverse('submissions:assignment_request', args=(self.id,))
@property
def notification_name(self):
return self.submission.arxiv_identifier_w_vn_nr
[docs]class RefereeInvitation(SubmissionRelatedObjectMixin, models.Model):
submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
related_name='referee_invitations')
referee = models.ForeignKey('scipost.Contributor', related_name='referee_invitations',
blank=True, null=True, on_delete=models.CASCADE)
title = models.CharField(max_length=4, choices=TITLE_CHOICES)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
email_address = models.EmailField()
# if Contributor not found, person is invited to register
invitation_key = models.CharField(max_length=40)
date_invited = models.DateTimeField(default=timezone.now)
invited_by = models.ForeignKey('scipost.Contributor', related_name='referee_invited_by',
blank=True, null=True, on_delete=models.CASCADE)
nr_reminders = models.PositiveSmallIntegerField(default=0)
date_last_reminded = models.DateTimeField(blank=True, null=True)
accepted = models.NullBooleanField(choices=ASSIGNMENT_NULLBOOL, default=None)
date_responded = models.DateTimeField(blank=True, null=True)
refusal_reason = models.CharField(max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS,
blank=True, null=True)
fulfilled = models.BooleanField(default=False) # True if a Report has been submitted
cancelled = models.BooleanField(default=False) # True if EIC has deactivated invitation
objects = RefereeInvitationQuerySet.as_manager()
def __str__(self):
return (self.first_name + ' ' + self.last_name + ' to referee ' +
self.submission.title[:30] + ' by ' + self.submission.author_list[:30] +
', invited on ' + self.date_invited.strftime('%Y-%m-%d'))
def get_absolute_url(self):
return reverse('submissions:accept_or_decline_ref_invitations', args=(self.id,))
@property
def referee_str(self):
if self.referee:
return str(self.referee)
return self.last_name + ', ' + self.first_name
@property
def notification_name(self):
return self.submission.arxiv_identifier_w_vn_nr
@property
def related_report(self):
return self.submission.reports.filter(author=self.referee).first()
def reset_content(self):
self.nr_reminders = 0
self.date_last_reminded = None
self.accepted = None
self.refusal_reason = None
self.fulfilled = False
self.cancelled = False
###########
# Reports:
###########
[docs]class Report(SubmissionRelatedObjectMixin, models.Model):
"""
Both types of reports, invited or contributed.
This Report model acts as both a regular `Report` and a `FollowupReport`; A normal Report
should have all fields required, whereas a FollowupReport only has the `report` field as
a required field.
Important note!
Due to the construction of the two different types within a single model, it is important
to explicitly implement the perticular differences in for example the form used.
"""
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)
date_submitted = models.DateTimeField('date submitted')
author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE,
related_name='reports')
qualification = models.PositiveSmallIntegerField(
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()
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(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)
objects = ReportQuerySet.as_manager()
class Meta:
unique_together = ('submission', 'report_nr')
default_related_name = 'reports'
ordering = ['-date_submitted']
def __str__(self):
return (self.author.user.first_name + ' ' + self.author.user.last_name + ' on ' +
self.submission.title[:50] + ' by ' + self.submission.author_list[:50])
@property
def is_in_draft(self):
return self.status == STATUS_DRAFT
@property
def is_vetted(self):
return self.status == STATUS_VETTED
def save(self, *args, **kwargs):
# Control Report count per Submission.
if not self.report_nr:
self.report_nr = self.submission.reports.count() + 1
return super().save(*args, **kwargs)
def create_doi_label(self):
self.doi_label = 'SciPost.Report.' + str(self.id)
self.save()
def get_absolute_url(self):
return self.submission.get_absolute_url() + '#report_' + str(self.report_nr)
@property
def notification_name(self):
return self.submission.arxiv_identifier_w_vn_nr
@property
def doi_string(self):
if self.doi_label:
return '10.21468/' + self.doi_label
return ''
@cached_property
def title(self):
"""
This property is (mainly) used to let Comments get the title of the Submission without
annoying logic.
"""
return self.submission.title
@property
def is_followup_report(self):
"""
Check if current Report is a `FollowupReport`. A Report is a `FollowupReport` if the
author of the report already has a vetted report in the series of the specific Submission.
"""
return (self.author.reports.accepted().filter(
submission__arxiv_identifier_wo_vn_nr=self.submission.arxiv_identifier_wo_vn_nr,
submission__arxiv_vn_nr__lt=self.submission.arxiv_vn_nr).exists())
[docs] def latest_report_from_series(self):
"""
Get latest Report from the same author for the Submission series.
"""
return (self.author.reports.accepted().filter(
submission__arxiv_identifier_wo_vn_nr=self.submission.arxiv_identifier_wo_vn_nr)
.order_by('submission__arxiv_identifier_wo_vn_nr').last())
@property
def associated_published_doi(self):
"""
Check if the Report relates to a SciPost-published object.
If it is, return the doi of the published object.
"""
try:
publication = Publication.objects.get(
accepted_submission__arxiv_identifier_wo_vn_nr=self.submission.arxiv_identifier_wo_vn_nr)
except Publication.DoesNotExist:
return None
return publication.doi_string
@property
def relation_to_published(self):
"""
Check if the Report relates to a SciPost-published object.
If it is, 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__arxiv_identifier_wo_vn_nr=self.submission.arxiv_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.arxiv_identifier_w_vn_nr,
'contributor_role': 'reviewer',
}
return relation
@property
def citation(self):
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.arxiv_identifier_w_vn_nr
citation += 'delivered %s, ' % self.date_submitted.strftime('%Y-%m-%d')
citation += 'doi: %s' % self.doi_string
return citation
##########################
# EditorialCommunication #
##########################
[docs]class EditorialCommunication(SubmissionRelatedObjectMixin, models.Model):
"""
Each individual communication between Editor-in-charge
to and from Referees and Authors becomes an instance of this class.
"""
submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE)
referee = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE,
blank=True, null=True)
comtype = models.CharField(max_length=4, choices=ED_COMM_CHOICES)
timestamp = models.DateTimeField(default=timezone.now)
text = models.TextField()
objects = EditorialCommunicationQueryset.as_manager()
class Meta:
ordering = ['timestamp']
default_related_name = 'editorial_communications'
def __str__(self):
output = self.comtype
if self.referee is not None:
output += ' ' + self.referee.user.first_name + ' ' + self.referee.user.last_name
output += (' for submission ' + self.submission.title[:30] + ' by '
+ self.submission.author_list[:30])
return output
[docs]class EICRecommendation(SubmissionRelatedObjectMixin, models.Model):
"""
The EICRecommendation is the recommendation of a Submission written by
the Editor-in-charge made at the end of the refereeing cycle. It can be voted for by
a subset of Fellows and should contain the actual publication decision.
"""
submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
related_name='eicrecommendations')
date_submitted = models.DateTimeField('date submitted', default=timezone.now)
remarks_for_authors = models.TextField(blank=True, null=True)
requested_changes = models.TextField(verbose_name="requested changes", blank=True, null=True)
remarks_for_editorial_college = models.TextField(blank=True,
verbose_name='optional remarks for the'
' Editorial College')
recommendation = models.SmallIntegerField(choices=REPORT_REC)
version = models.SmallIntegerField(default=1)
active = models.BooleanField(default=True)
# Editorial Fellows who have assessed this recommendation:
eligible_to_vote = models.ManyToManyField('scipost.Contributor', blank=True,
related_name='eligible_to_vote')
voted_for = models.ManyToManyField('scipost.Contributor', blank=True, related_name='voted_for')
voted_against = models.ManyToManyField('scipost.Contributor', blank=True,
related_name='voted_against')
voted_abstain = models.ManyToManyField('scipost.Contributor', blank=True,
related_name='voted_abstain')
voting_deadline = models.DateTimeField('date submitted', default=timezone.now)
objects = EICRecommendationQuerySet.as_manager()
class Meta:
unique_together = ('submission', 'version')
ordering = ['version']
def __str__(self):
return '{title} by {author}, {recommendation} version {version}'.format(
title=self.submission.title[:20],
author=self.submission.author_list[:30],
recommendation=self.get_recommendation_display(),
version=self.version,
)
def get_absolute_url(self):
# TODO: Fix this weird redirect, but it's neccesary for the notifications to have one.
return self.submission.get_absolute_url()
@property
def notification_name(self):
return self.submission.arxiv_identifier_w_vn_nr
@property
def nr_for(self):
return self.voted_for.count()
@property
def nr_against(self):
return self.voted_against.count()
@property
def nr_abstained(self):
return self.voted_abstain.count()
def may_be_reformulated(self):
if not self.active:
# Already reformulated before; please use the latest version
return False
return self.submission.status in [STATUS_VOTING_IN_PREPARATION, STATUS_PUT_TO_EC_VOTING]
[docs]class iThenticateReport(TimeStampedModel):
"""
iThenticateReport is the SciPost register of an iThenticate report. It saves
basic information coming from iThenticate into the SciPost database for easy access.
"""
uploaded_time = models.DateTimeField(null=True, blank=True)
processed_time = models.DateTimeField(null=True, blank=True)
doc_id = models.IntegerField(primary_key=True)
part_id = models.IntegerField(null=True, blank=True)
percent_match = models.IntegerField(null=True, blank=True)
class Meta:
verbose_name = 'iThenticate Report'
verbose_name_plural = 'iThenticate Reports'
def get_absolute_url(self):
if hasattr(self, 'to_submission'):
return reverse('submissions:plagiarism', kwargs={
'arxiv_identifier_w_vn_nr':
self.to_submission.arxiv_identifier_w_vn_nr})
return None
[docs] def get_report_url(self):
"""
Request new read-only url from iThenticate and return.
Note: The read-only link is valid for only 15 minutes, saving may be worthless
"""
if not self.part_id:
return ''
from .plagiarism import iThenticate
plagiarism = iThenticate()
return plagiarism.get_url(self.part_id)
def __str__(self):
_str = 'Report {doc_id}'.format(doc_id=self.doc_id)
if hasattr(self, 'to_submission'):
_str += ' on Submission {arxiv}'.format(
arxiv=self.to_submission.arxiv_identifier_w_vn_nr)
return _str
def save(self, *args, **kwargs):
obj = super().save(*args, **kwargs)
if hasattr(self, 'to_submission') and kwargs.get('commit', True):
self.to_submission.touch()
return obj
@property
def score(self):
return self.percent_match