Source code for submissions.forms
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import datetime
import re
from django import forms
from django.conf import settings
from django.contrib import messages
# from django.contrib.postgres.search import TrigramSimilarity
from django.db import transaction
from django.db.models import Q
from django.forms.formsets import ORDERING_FIELD_NAME
from django.utils import timezone
from .constants import (
ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED, REPORT_ACTION_CHOICES,
REPORT_REFUSAL_CHOICES, STATUS_REJECTED, STATUS_INCOMING, REPORT_POST_EDREC, REPORT_NORMAL,
STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE, STATUS_UNASSIGNED,
EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS, PUT_TO_VOTING, CYCLE_UNDETERMINED,
SUBMISSION_CYCLE_CHOICES,
EIC_REC_PUBLISH, EIC_REC_MINOR_REVISION, EIC_REC_MAJOR_REVISION, EIC_REC_REJECT,
ALT_REC_CHOICES, SUBMISSION_TIERS,
STATUS_VETTED, DECISION_FIXED, DEPRECATED, STATUS_COMPLETED,
STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC, STATUS_PREASSIGNED, STATUS_REPLACED,
STATUS_FAILED_PRESCREENING, STATUS_DEPRECATED,
STATUS_ACCEPTED, STATUS_DECLINED, STATUS_WITHDRAWN)
from . import exceptions, helpers
from .helpers import to_ascii_only
from .models import (
Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment,
SubmissionTiering, EditorialDecision,
iThenticateReport, EditorialCommunication)
from .signals import notify_manuscript_accepted
from colleges.models import Fellowship
from common.utils import Q_with_alternative_spellings
from journals.models import Journal
from mails.utils import DirectMailUtil
from preprints.helpers import generate_new_scipost_identifier
from preprints.models import Preprint
from production.utils import get_or_create_production_stream
from profiles.models import Profile
from scipost.constants import SCIPOST_SUBJECT_AREAS
from scipost.services import ArxivCaller
from scipost.models import Contributor, Remark
import strings
import iThenticate
IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}\.[0-9]{4,5}v[0-9]{1,2}$'
[docs]class SubmissionSearchForm(forms.Form):
"""Filter a Submission queryset using basic search fields."""
author = forms.CharField(max_length=100, required=False, label="Author(s)")
title = forms.CharField(max_length=100, required=False)
abstract = forms.CharField(max_length=1000, required=False)
subject_area = forms.CharField(max_length=10, required=False, widget=forms.Select(
choices=((None, 'Show all'),) + SCIPOST_SUBJECT_AREAS[0][1]))
[docs] def search_results(self):
"""Return all Submission objects according to search."""
return Submission.objects.public_newest().filter(
title__icontains=self.cleaned_data.get('title', ''),
author_list__icontains=self.cleaned_data.get('author', ''),
abstract__icontains=self.cleaned_data.get('abstract', ''),
subject_area__icontains=self.cleaned_data.get('subject_area', '')
)
[docs]class SubmissionPoolFilterForm(forms.Form):
status = forms.ChoiceField(
choices=((None, 'All submissions currently under evaluation'),) + SUBMISSION_STATUS,
required=False)
editor_in_charge = forms.BooleanField(
label='Show only Submissions for which I am editor in charge.', required=False)
[docs] def search(self, queryset, current_user):
if self.cleaned_data.get('status'):
# Do extra check on non-required field to never show errors on template
queryset = queryset.pool_editable(current_user).filter(
status=self.cleaned_data['status'])
else:
# If no specific status if requested, just return the Pool by default
queryset = queryset.pool(current_user)
if self.cleaned_data.get('editor_in_charge') and hasattr(current_user, 'contributor'):
queryset = queryset.filter(editor_in_charge=current_user.contributor)
return queryset.order_by('-submission_date')
[docs] def status_verbose(self):
try:
return dict(SUBMISSION_STATUS)[self.cleaned_data['status']]
except KeyError:
return ''
###############################
# Submission and resubmission #
###############################
[docs]class SubmissionService:
"""
Object to run checks for prefiller and submit manuscript forms.
"""
metadata = {}
def __init__(self, requested_by, preprint_server, identifier=None, resubmission_of_id=None):
self.requested_by = requested_by
self.preprint_server = preprint_server
self.identifier = identifier
self.resubmission_of_id = resubmission_of_id
self._arxiv_data = None
@property
def latest_submission(self):
"""
Return latest version of preprint series or None.
"""
if hasattr(self, '_latest_submission'):
return self._latest_submission
if self.identifier:
# Check if is resubmission when identifier data is submitted.
identifier = self.identifier.rpartition('v')[0]
self._latest_submission = Submission.objects.filter(
preprint__identifier_wo_vn_nr=identifier).order_by(
'-preprint__vn_nr').first()
elif self.resubmission_of_id:
# Resubmission (submission id) is selected by user.
try:
self._latest_submission = Submission.objects.filter(
id=int(self.resubmission_of_id)).order_by('-preprint__vn_nr').first()
except ValueError:
self._latest_submission = None
else:
self._latest_submission = None
return self._latest_submission
@property
def arxiv_data(self):
if self._arxiv_data is None:
self._call_arxiv()
return self._arxiv_data
[docs] def run_checks(self):
"""
Do several pre-checks (using the arXiv API if needed).
This is needed for both the prefill and submission forms.
"""
self._submission_already_exists()
self._submission_previous_version_is_valid_for_submission()
if self.preprint_server == 'arxiv':
self._submission_is_already_published()
def _call_arxiv(self):
"""
Retrieve all data from the ArXiv database for `identifier`.
"""
if self.preprint_server != 'arxiv':
# Do the call here to prevent multiple calls to the arXiv API in one request.
self._arxiv_data = {}
return
if not self.identifier:
print('crap', self.identifier)
return
caller = ArxivCaller(self.identifier)
if caller.is_valid:
self._arxiv_data = caller.data
self.metadata = caller.metadata
else:
error_message = 'A preprint associated to this identifier does not exist.'
raise forms.ValidationError(error_message)
[docs] def get_latest_submission_data(self):
"""
Return initial form data originating from earlier Submission.
"""
if self.is_resubmission():
return {
'title': self.latest_submission.title,
'abstract': self.latest_submission.abstract,
'author_list': self.latest_submission.author_list,
'discipline': self.latest_submission.discipline,
'approaches': self.latest_submission.approaches,
'referees_flagged': self.latest_submission.referees_flagged,
'referees_suggested': self.latest_submission.referees_suggested,
'secondary_areas': self.latest_submission.secondary_areas,
'subject_area': self.latest_submission.subject_area,
'submitted_to': self.latest_submission.submitted_to,
'submission_type': self.latest_submission.submission_type,
}
return {}
[docs] def is_resubmission(self):
"""
Check if Submission is a SciPost or arXiv resubmission.
"""
return self.latest_submission is not None
[docs] def identifier_matches_regex(self, journal_code):
"""
Check if identifier is valid for the Journal submitting to.
"""
if self.preprint_server != 'arxiv':
# Only check arXiv identifiers
return
if journal_code in EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS.keys():
regex = EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS[journal_code]
else:
regex = EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS['default']
pattern = re.compile(regex)
if not pattern.match(self.identifier):
# No match object returned, identifier is invalid
error_message = ('The journal you want to submit to does not allow for this'
' identifier. Please contact SciPost if you have'
' any further questions.')
raise forms.ValidationError(error_message, code='submitted_to')
[docs] def process_resubmission_procedure(self, submission):
"""
Update all fields for new and old Submission and EditorialAssignments to comply with
the resubmission procedures.
-- submission: the new version of the Submission series.
"""
if not self.latest_submission:
raise Submission.DoesNotExist
# Close last submission
Submission.objects.filter(id=self.latest_submission.id).update(
is_current=False, open_for_reporting=False, status=STATUS_RESUBMITTED)
# Copy Topics
submission.topics.add(*self.latest_submission.topics.all())
# Open for comment and reporting and copy EIC info
Submission.objects.filter(id=submission.id).update(
open_for_reporting=True,
open_for_commenting=True,
is_resubmission_of=self.latest_submission,
visible_pool=True,
refereeing_cycle=CYCLE_UNDETERMINED,
editor_in_charge=self.latest_submission.editor_in_charge,
status=STATUS_EIC_ASSIGNED,
thread_hash=self.latest_submission.thread_hash)
# Add author(s) (claim) fields
submission.authors.add(*self.latest_submission.authors.all())
submission.authors_claims.add(*self.latest_submission.authors_claims.all())
submission.authors_false_claims.add(*self.latest_submission.authors_false_claims.all())
# Create new EditorialAssigment for the current Editor-in-Charge
EditorialAssignment.objects.create(
submission=submission,
to=self.latest_submission.editor_in_charge,
status=STATUS_ACCEPTED)
def _submission_already_exists(self):
"""
Check if preprint has already been submitted before.
"""
if Submission.objects.filter(preprint__identifier_w_vn_nr=self.identifier).exists():
error_message = 'This preprint version has already been submitted to SciPost.'
raise forms.ValidationError(error_message, code='duplicate')
def _submission_previous_version_is_valid_for_submission(self):
"""
Check if previous submitted versions have the appropriate status.
"""
if self.latest_submission:
if self.latest_submission.status == STATUS_REJECTED:
# Explicitly give rejected status warning.
error_message = ('This preprint has previously undergone refereeing '
'and has been rejected. Resubmission is only possible '
'if the manuscript has been substantially reworked into '
'a new submission with distinct identifier.')
raise forms.ValidationError(error_message)
elif self.latest_submission.open_for_resubmission:
# Check if verified author list contains current user.
if self.requested_by.contributor not in self.latest_submission.authors.all():
error_message = ('There exists a preprint with this identifier '
'but an earlier version number. Resubmission is only possible'
' if you are a registered author of this manuscript.')
raise forms.ValidationError(error_message)
else:
# Submission has not an appropriate status for resubmission.
error_message = ('There exists a preprint with this identifier '
'but an earlier version number, which is still undergoing '
'peer refereeing. '
'A resubmission can only be performed after request '
'from the Editor-in-charge. Please wait until the '
'closing of the previous refereeing round and '
'formulation of the Editorial Recommendation '
'before proceeding with a resubmission.')
raise forms.ValidationError(error_message)
def _submission_is_already_published(self):
"""
Check if preprint number is already registered with a DOI in the *ArXiv* database.
"""
published_id = None
if 'arxiv_doi' in self.arxiv_data:
published_id = self.arxiv_data['arxiv_doi']
elif 'arxiv_journal_ref' in self.arxiv_data:
published_id = self.arxiv_data['arxiv_journal_ref']
if published_id:
error_message = ('This paper has been published under DOI %(published_id)s'
'. Please comment on the published version.'),
raise forms.ValidationError(error_message, code='published',
params={'published_id': published_id})
[docs]class SubmissionForm(forms.ModelForm):
"""
Form to submit a new (re)Submission.
"""
identifier_w_vn_nr = forms.CharField(widget=forms.HiddenInput())
preprint_file = forms.FileField(
help_text=('Please submit the processed .pdf (not the source files; '
'these will only be required at the post-acceptance proofs stage)'))
[docs] class Meta:
model = Submission
fields = [
'is_resubmission_of',
'discipline',
'submitted_to',
'proceedings',
'submission_type',
'subject_area',
'secondary_areas',
'approaches',
'title',
'author_list',
'abstract',
'author_comments',
'list_of_changes',
'remarks_for_editors',
'referees_suggested',
'referees_flagged',
'arxiv_link',
]
widgets = {
'is_resubmission_of': forms.HiddenInput(),
'secondary_areas': forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS),
'arxiv_link': forms.TextInput(
attrs={'placeholder': 'ex.: arxiv.org/abs/1234.56789v1'}),
'remarks_for_editors': forms.Textarea(
attrs={'placeholder': 'Any private remarks (for the editors only)', 'rows': 5}),
'referees_suggested': forms.Textarea(
attrs={'placeholder': 'Optional: names of suggested referees', 'rows': 5}),
'referees_flagged': forms.Textarea(
attrs={
'placeholder': 'Optional: names of referees whose reports should be treated with caution (+ short reason)',
'rows': 5
}),
'author_comments': forms.Textarea(
attrs={'placeholder': 'Your resubmission letter (will be viewable online)'}),
'list_of_changes': forms.Textarea(
attrs={'placeholder': 'Give a point-by-point list of changes (will be viewable online)'}),
}
def __init__(self, *args, **kwargs):
self.requested_by = kwargs.pop('requested_by')
self.preprint_server = kwargs.pop('preprint_server', 'arxiv')
self.resubmission_preprint = kwargs['initial'].get('resubmission', False)
data = args[0] if len(args) > 1 else kwargs.get('data', {})
identifier = kwargs['initial'].get('identifier_w_vn_nr', None) or data.get('identifier_w_vn_nr')
self.service = SubmissionService(
self.requested_by, self.preprint_server,
identifier=identifier,
resubmission_of_id=self.resubmission_preprint)
if self.preprint_server == 'scipost':
kwargs['initial'] = self.service.get_latest_submission_data()
super().__init__(*args, **kwargs)
if not self.preprint_server == 'arxiv':
# No arXiv-specific data required.
del self.fields['identifier_w_vn_nr']
del self.fields['arxiv_link']
elif not self.preprint_server == 'scipost':
# No need for a file upload if user is not using the SciPost preprint server.
del self.fields['preprint_file']
# Find all submission allowed to be resubmitted by current user.
self.fields['is_resubmission_of'].queryset = Submission.objects.candidate_for_resubmission(
self.requested_by)
# Fill resubmission-dependent fields
if self.is_resubmission():
self.fields['is_resubmission_of'].initial = self.service.latest_submission
else:
# These fields are only available for resubmissions.
del self.fields['author_comments']
del self.fields['list_of_changes']
if not self.fields['is_resubmission_of'].initial:
# No intial nor submitted data found.
del self.fields['is_resubmission_of']
# Select Journal instances.
self.fields['submitted_to'].queryset = Journal.objects.active().exclude(name='SciPost Selections')
self.fields['submitted_to'].label = 'Journal: submit to'
# Proceedings submission fields
qs = self.fields['proceedings'].queryset.open_for_submission()
self.fields['proceedings'].queryset = qs
self.fields['proceedings'].empty_label = None
if not qs.exists():
# No proceedings issue to submit to, so adapt the form fields
self.fields['submitted_to'].queryset = self.fields['submitted_to'].queryset.exclude(
doi_label__contains='Proc')
del self.fields['proceedings']
[docs] def clean(self, *args, **kwargs):
"""
Do all general checks for Submission.
"""
cleaned_data = super().clean(*args, **kwargs)
# SciPost preprints are auto-generated here.
self.scipost_identifier = None
if 'identifier_w_vn_nr' not in cleaned_data:
self.service.identifier, self.scipost_identifier = generate_new_scipost_identifier(
cleaned_data.get('is_resubmission_of', None))
# Also copy to the form data
self.cleaned_data['identifier_w_vn_nr'] = self.service.identifier
# Run checks again to clean any possible human intervention and run checks again
# with possibly newly generated identifier.
self.service.run_checks()
self.service.identifier_matches_regex(cleaned_data['submitted_to'].doi_label)
if 'Proc' not in self.cleaned_data['submitted_to'].doi_label:
try:
del self.cleaned_data['proceedings']
except KeyError:
# No proceedings returned to data
return cleaned_data
return cleaned_data
[docs] def clean_author_list(self):
"""
Check if author list matches the Contributor submitting.
"""
author_list = self.cleaned_data['author_list']
# Remove punctuation and convert to ASCII-only string.
clean_author_name = to_ascii_only(self.requested_by.last_name)
clean_author_list = to_ascii_only(author_list)
if not clean_author_name in clean_author_list:
error_message = ('Your name does not match that of any of the authors. '
'You are not authorized to submit this preprint.')
self.add_error('author_list', error_message)
return author_list
[docs] def clean_submission_type(self):
"""
Validate Submission type for the SciPost Physics journal.
"""
submission_type = self.cleaned_data['submission_type']
journal_doi_label = self.cleaned_data['submitted_to'].doi_label
if journal_doi_label == 'SciPostPhys' and not submission_type:
self.add_error('submission_type', 'Please specify the submission type.')
return submission_type
[docs] def set_pool(self, submission):
"""
Set the default set of (guest) Fellows for this Submission.
"""
qs = Fellowship.objects.active()
fellows = qs.regular().filter(
contributor__discipline=submission.discipline).return_active_for_submission(submission)
submission.fellows.set(fellows)
if submission.proceedings:
# Add Guest Fellowships if the Submission is a Proceedings manuscript
guest_fellows = qs.guests().filter(
proceedings=submission.proceedings).return_active_for_submission(submission)
submission.fellows.add(*guest_fellows)
[docs] @transaction.atomic
def save(self):
"""
Create the new Submission and Preprint instances.
"""
submission = super().save(commit=False)
submission.submitted_by = self.requested_by.contributor
# Save identifiers
identifiers = self.cleaned_data['identifier_w_vn_nr'].rpartition('v')
preprint, __ = Preprint.objects.get_or_create(
identifier_w_vn_nr=self.cleaned_data['identifier_w_vn_nr'],
identifier_wo_vn_nr=identifiers[0],
vn_nr=identifiers[2],
url=self.cleaned_data.get('arxiv_link', ''),
scipost_preprint_identifier=self.scipost_identifier,
_file=self.cleaned_data.get('preprint_file', None), )
# Save metadata directly from ArXiv call without possible user interception
submission.metadata = self.service.metadata
submission.preprint = preprint
submission.save()
if self.is_resubmission():
self.service.process_resubmission_procedure(submission)
# Gather first known author and Fellows.
submission.authors.add(self.requested_by.contributor)
self.set_pool(submission)
# Return latest version of the Submission. It could be outdated by now.
submission.refresh_from_db()
return submission
[docs]class SubmissionIdentifierForm(forms.Form):
"""
Prefill SubmissionForm using this form that takes an arXiv ID only.
"""
IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)'
identifier_w_vn_nr = forms.RegexField(
label='arXiv identifier with version number',
regex=IDENTIFIER_PATTERN_NEW, strip=True,
error_messages={'invalid': strings.arxiv_query_invalid},
widget=forms.TextInput({'placeholder': IDENTIFIER_PLACEHOLDER}))
def __init__(self, *args, **kwargs):
self.requested_by = kwargs.pop('requested_by')
return super().__init__(*args, **kwargs)
[docs] def clean_identifier_w_vn_nr(self):
"""
Do basic prechecks based on the arXiv ID only.
"""
identifier = self.cleaned_data.get('identifier_w_vn_nr', None)
self.service = SubmissionService(self.requested_by, 'arxiv', identifier=identifier)
self.service.run_checks()
return identifier
[docs] def get_initial_submission_data(self):
"""
Return dictionary to prefill `SubmissionForm`.
"""
form_data = self.service.arxiv_data
form_data['identifier_w_vn_nr'] = self.cleaned_data['identifier_w_vn_nr']
if self.service.is_resubmission():
form_data.update({
'discipline': self.service.latest_submission.discipline,
'approaches': self.service.latest_submission.approaches,
'referees_flagged': self.service.latest_submission.referees_flagged,
'referees_suggested': self.service.latest_submission.referees_suggested,
'secondary_areas': self.service.latest_submission.secondary_areas,
'subject_area': self.service.latest_submission.subject_area,
'submitted_to': self.service.latest_submission.submitted_to,
'submission_type': self.service.latest_submission.submission_type,
})
return form_data
[docs]class PreassignEditorsForm(forms.ModelForm):
"""Preassign editors for incoming Submission."""
assign = forms.BooleanField(required=False)
to = forms.ModelChoiceField(
queryset=Contributor.objects.none(), required=True, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
self.fields['to'].queryset = Contributor.objects.filter(
fellowships__in=self.submission.fellows.all())
self.fields['assign'].initial = self.instance.id is not None
[docs] def save(self, commit=True):
"""Create/get unordered EditorialAssignments or delete existing if needed."""
if self.cleaned_data['assign']:
# Create/save
self.instance, __ = EditorialAssignment.objects.get_or_create(
submission=self.submission, to=self.cleaned_data['to'])
elif self.instance.id is not None:
# Delete if exists
if self.instance.status == STATUS_PREASSIGNED:
self.instance.delete()
return self.instance
[docs] def get_fellow(self):
"""Get fellow either via initial data or instance."""
if self.instance.id is not None:
return self.instance.to
return self.initial.get('to', None)
[docs]class BasePreassignEditorsFormSet(forms.BaseModelFormSet):
"""Preassign editors for incoming Submission."""
def __init__(self, *args, **kwargs):
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
self.queryset = self.submission.editorial_assignments.order_by('invitation_order')
# Prefill form fields and create unassigned rows for unassigned fellows.
assigned_fellows = self.submission.fellows.filter(
contributor__editorial_assignments__in=self.queryset)
unassigned_fellows = self.submission.fellows.exclude(
contributor__editorial_assignments__in=self.queryset)
possible_assignments = [{ORDERING_FIELD_NAME: -1} for fellow in assigned_fellows]
for fellow in unassigned_fellows:
possible_assignments.append({
'submission': self.submission, 'to': fellow.contributor, ORDERING_FIELD_NAME: -1})
self.initial = possible_assignments
self.extra += len(unassigned_fellows)
[docs] def add_fields(self, form, index):
"""Force hidden input for ORDER field."""
super().add_fields(form, index)
if ORDERING_FIELD_NAME in form.fields:
form.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()
[docs] def get_form_kwargs(self, index):
"""Add submission to form arguments."""
kwargs = super().get_form_kwargs(index)
kwargs['submission'] = self.submission
return kwargs
[docs] def save(self, commit=True):
"""Save each form and order EditorialAssignments."""
objects = super().save(commit=False)
objects = []
count = 0
for form in self.ordered_forms:
ed_assignment = form.save()
if ed_assignment.id is None:
continue
count += 1
EditorialAssignment.objects.filter(id=ed_assignment.id).update(invitation_order=count)
objects.append(ed_assignment)
return objects
PreassignEditorsFormSet = forms.modelformset_factory(
EditorialAssignment, can_order=True, extra=0,
formset=BasePreassignEditorsFormSet, form=PreassignEditorsForm)
[docs]class SubmissionReassignmentForm(forms.ModelForm):
"""Process reassignment of EIC for Submission."""
new_editor = forms.ModelChoiceField(queryset=Contributor.objects.none(), required=True)
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
self.fields['new_editor'].queryset = Contributor.objects.filter(
fellowships__in=self.submission.fellows.all()).exclude(
id=self.submission.editor_in_charge.id)
[docs] def save(self):
"""Update old/create new Assignment and send mails."""
old_editor = self.submission.editor_in_charge
old_assignment = self.submission.editorial_assignments.ongoing().filter(
to=old_editor).first()
if old_assignment:
EditorialAssignment.objects.filter(id=old_assignment.id).update(status=STATUS_REPLACED)
# Update Submission and update/create Editorial Assignments
now = timezone.now()
assignment = EditorialAssignment.objects.create(
submission=self.submission,
to=self.cleaned_data['new_editor'],
status=STATUS_ACCEPTED,
date_invited=now,
date_answered=now,
)
self.submission.editor_in_charge = self.cleaned_data['new_editor']
self.submission.save()
# Email old and new editor
if old_assignment:
mail_sender = DirectMailUtil(
'fellows/email_fellow_replaced_by_other', assignment=old_assignment)
mail_sender.send_mail()
mail_sender = DirectMailUtil(
'fellows/email_fellow_assigned_submission',
assignment=assignment)
mail_sender.send_mail()
[docs]class SubmissionPrescreeningForm(forms.ModelForm):
"""Processing decision for pre-screening of Submission."""
PASS, FAIL = 'pass', 'fail'
CHOICES = (
(PASS, 'Pass pre-screening. Proceed to the Pool.'),
(FAIL, 'Fail pre-screening.'))
decision = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES, required=False)
message_for_authors = forms.CharField(required=False, widget=forms.Textarea({
'placeholder': 'Message for authors'}))
remark_for_pool = forms.CharField(required=False, widget=forms.Textarea({
'placeholder': 'Remark for the pool'}))
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
self.current_user = kwargs.pop('current_user')
super().__init__(*args, **kwargs)
[docs] def clean(self):
"""Check if Submission has right status."""
data = super().clean()
if self.instance.status != STATUS_INCOMING:
self.add_error(None, 'This Submission is currently not in pre-screening.')
if data['decision'] == self.PASS:
if not self.instance.fellows.exists():
self.add_error(None, 'Please add at least one fellow to the pool first.')
if not self.instance.editorial_assignments.exists():
self.add_error(None, 'Please complete the pre-assignments form first.')
return data
[docs] @transaction.atomic
def save(self):
"""Update Submission status."""
if self.cleaned_data['decision'] == self.PASS:
Submission.objects.filter(id=self.instance.id).update(
status=STATUS_UNASSIGNED, visible_pool=True, visible_public=False)
self.instance.add_general_event('Submission passed pre-screening.')
elif self.cleaned_data['decision'] == self.FAIL:
Submission.objects.filter(id=self.instance.id).update(
status=STATUS_FAILED_PRESCREENING, visible_pool=False, visible_public=False)
self.instance.add_general_event('Submission failed pre-screening.')
if self.cleaned_data['remark_for_pool']:
Remark.objects.create(
submission=self.instance,
contributor=self.current_user.contributor,
remark=self.cleaned_data['remark_for_pool'])
if self.cleaned_data['message_for_authors']:
pass
[docs]class WithdrawSubmissionForm(forms.Form):
"""
A submitting author has the right to withdraw the manuscript.
"""
confirm = forms.ChoiceField(
widget=forms.RadioSelect, choices=((True, 'Confirm'), (False, 'Abort')), label='')
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
[docs] def save(self):
if self.is_confirmed():
# Update submission (current + any previous versions)
Submission.objects.filter(id=self.submission.id).update(
visible_public=False, visible_pool=False,
open_for_commenting=False, open_for_reporting=False,
status=STATUS_WITHDRAWN, latest_activity=timezone.now())
self.submission.get_other_versions().update(visible_public=False)
# Update all assignments
EditorialAssignment.objects.filter(submission=self.submission).need_response().update(
status=STATUS_DEPRECATED)
EditorialAssignment.objects.filter(submission=self.submission).accepted().update(
status=STATUS_COMPLETED)
# Deprecate any outstanding recommendations
if EICRecommendation.objects.filter(submission=self.submission).exists():
EICRecommendation.objects.filter(submission=self.submission).active().update(
status=DEPRECATED)
# Update editorial decision
if EditorialDecision.objects.filter(submission=self.submission).exists():
EditorialDecision.objects.filter(submission=self.submission).last().update(
status=EditorialDecision.PUBOFFER_REFUSED_BY_AUTHORS)
# Delete any production stream
if hasattr(self.submission, 'production_stream'):
self.submission.production_stream.delete()
self.submission.refresh_from_db()
return self.submission
######################
# Editorial workflow #
######################
[docs]class InviteEditorialAssignmentForm(forms.ModelForm):
"""Invite new Fellow; create EditorialAssignment for Submission."""
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
self.fields['to'].queryset = Contributor.objects.available().filter(
fellowships__pool=self.submission).distinct().order_by('user__last_name')
[docs] def save(self, commit=True):
self.instance.submission = self.submission
return super().save(commit)
[docs]class EditorialAssignmentForm(forms.ModelForm):
"""Create and/or process new EditorialAssignment for Submission."""
DECISION_CHOICES = (
('accept', 'Accept'),
('decline', 'Decline'))
CYCLE_CHOICES = (
(CYCLE_DEFAULT, 'Normal refereeing cycle'),
(CYCLE_DIRECT_REC, 'Directly formulate Editorial Recommendation for rejection'))
decision = forms.ChoiceField(
widget=forms.RadioSelect, choices=DECISION_CHOICES,
label="Are you willing to take charge of this Submission?")
refereeing_cycle = forms.ChoiceField(
widget=forms.RadioSelect, choices=CYCLE_CHOICES, initial=CYCLE_DEFAULT)
refusal_reason = forms.ChoiceField(
choices=ASSIGNMENT_REFUSAL_REASONS)
[docs] class Meta:
model = EditorialAssignment
fields = () # Don't use the default fields options because of the ordering of fields.
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
self.request = kwargs.pop('request')
super().__init__(*args, **kwargs)
if not self.instance.id:
del self.fields['decision']
del self.fields['refusal_reason']
[docs] def has_accepted_invite(self):
"""Check if invite is accepted or if voluntered to become EIC."""
return 'decision' not in self.cleaned_data or self.cleaned_data['decision'] == 'accept'
[docs] def is_normal_cycle(self):
"""Check if normal refereeing cycle is chosen."""
return self.cleaned_data['refereeing_cycle'] == CYCLE_DEFAULT
[docs] def save(self, commit=True):
"""Save Submission to EditorialAssignment."""
self.instance.submission = self.submission
self.instance.date_answered = timezone.now()
self.instance.to = self.request.user.contributor
assignment = super().save() # Save already, in case it's a new recommendation.
if self.has_accepted_invite():
# Update related Submission.
if self.is_normal_cycle():
# Default Refereeing process
deadline = timezone.now() + self.instance.submission.submitted_to.refereeing_period
# Update related Submission.
Submission.objects.filter(id=self.submission.id).update(
refereeing_cycle=CYCLE_DEFAULT,
status=STATUS_EIC_ASSIGNED,
editor_in_charge=self.request.user.contributor,
reporting_deadline=deadline,
open_for_reporting=True,
open_for_commenting=True,
visible_public=True,
latest_activity=timezone.now())
# Refresh the instance
self.instance.submission = Submission.objects.get(id=self.submission.id)
else:
# Short Refereeing process
Submission.objects.filter(id=self.submission.id).update(
refereeing_cycle=CYCLE_DIRECT_REC,
status=STATUS_EIC_ASSIGNED,
editor_in_charge=self.request.user.contributor,
reporting_deadline=timezone.now(),
open_for_reporting=False,
open_for_commenting=True,
visible_public=False,
latest_activity=timezone.now())
# Refresh the instance
self.instance.submission = Submission.objects.get(id=self.submission.id)
# Implicitly or explicity accept the assignment and deprecate others.
# assignment.accepted = True # Deprecated field
assignment.status = STATUS_ACCEPTED
# Update all other 'open' invitations
EditorialAssignment.objects.filter(submission=self.submission).need_response().exclude(
id=assignment.id).update(status=STATUS_DEPRECATED)
else:
# assignment.accepted = False # Deprecated field
assignment.status = STATUS_DECLINED
assignment.refusal_reason = self.cleaned_data['refusal_reason']
assignment.save() # Save again to register acceptance
return assignment
[docs]class ConsiderAssignmentForm(forms.Form):
"""Process open EditorialAssignment."""
accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL,
label="Are you willing to take charge of this Submission?")
refusal_reason = forms.ChoiceField(choices=ASSIGNMENT_REFUSAL_REASONS, required=False)
[docs]class RefereeSearchForm(forms.Form):
last_name = forms.CharField(widget=forms.TextInput({
'placeholder': 'Search for a referee in the SciPost Profiles database'}))
[docs] def search(self):
query = Q_with_alternative_spellings(
last_name__icontains=self.cleaned_data['last_name'])
return Profile.objects.filter(query)
# return Profile.objects.annotate(
# similarity=TrigramSimilarity('last_name', self.cleaned_data['last_name']),
# ).filter(similarity__gt=0.3).order_by('-similarity')
[docs]class ConsiderRefereeInvitationForm(forms.Form):
accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL,
label="Are you willing to referee this Submission?")
refusal_reason = forms.ChoiceField(choices=ASSIGNMENT_REFUSAL_REASONS, required=False)
[docs]class SetRefereeingDeadlineForm(forms.Form):
deadline = forms.DateField(
required=False, label='', widget=forms.SelectDateWidget(
years=[timezone.now().year + i for i in range(2)],
empty_label=("Year", "Month", "Day"),
))
[docs] def clean_deadline(self):
if not self.cleaned_data.get('deadline'):
self.add_error('deadline', 'Please use a valid date.')
return self.cleaned_data.get('deadline')
[docs]class VotingEligibilityForm(forms.ModelForm):
"""Assign Fellows to vote for EICRecommendation and open its status for voting."""
eligible_fellows = forms.ModelMultipleChoiceField(
queryset=Contributor.objects.none(),
widget=forms.CheckboxSelectMultiple(),
required=True, label='Eligible for voting')
def __init__(self, *args, **kwargs):
"""Get queryset of Contributors eligible for voting."""
super().__init__(*args, **kwargs)
secondary_areas = self.instance.submission.secondary_areas
if not secondary_areas:
secondary_areas = []
# If there exists a previous recommendation, include previous voting Fellows:
prev_elig_id = []
for prev_rec in self.instance.submission.eicrecommendations.all():
prev_elig_id += [fellow.id for fellow in prev_rec.eligible_to_vote.all()]
eligible = Contributor.objects.filter(
fellowships__pool=self.instance.submission).filter(
Q(EIC=self.instance.submission) |
Q(expertises__contains=[self.instance.submission.subject_area]) |
Q(expertises__contains=secondary_areas) |
Q(pk__in=prev_elig_id)
).order_by('user__last_name').distinct()
self.fields['eligible_fellows'].queryset = eligible
[docs] def save(self, commit=True):
"""Update EICRecommendation status and save its voters."""
self.instance.eligible_to_vote.set(self.cleaned_data['eligible_fellows'])
self.instance.status = PUT_TO_VOTING
if commit:
self.instance.save()
self.instance.submission.touch()
self.instance.voted_for.add(self.instance.submission.editor_in_charge)
return self.instance
############
# Reports:
############
[docs]class ReportForm(forms.ModelForm):
"""Write Report form."""
report_type = REPORT_NORMAL
[docs] class Meta:
model = Report
fields = ['qualification', 'strengths', 'weaknesses', 'report', 'requested_changes',
'validity', 'significance', 'originality', 'clarity', 'formatting', 'grammar',
'recommendation', 'remarks_for_editors', 'file_attachment', 'anonymous']
def __init__(self, *args, **kwargs):
if kwargs.get('instance'):
if kwargs['instance'].is_followup_report:
# Prefill data from latest report in the series
latest_report = kwargs['instance'].latest_report_from_thread()
kwargs.update({
'initial': {
'qualification': latest_report.qualification,
'anonymous': latest_report.anonymous
}
})
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
self.fields['strengths'].widget.attrs.update({
'placeholder': ('Give a point-by-point '
'(numbered 1-, 2-, ...) list of the paper\'s strengths'),
'rows': 10,
'cols': 100
})
self.fields['weaknesses'].widget.attrs.update({
'placeholder': ('Give a point-by-point '
'(numbered 1-, 2-, ...) list of the paper\'s weaknesses'),
'rows': 10,
'cols': 100
})
self.fields['report'].widget.attrs.update({'placeholder': 'Your general remarks',
'rows': 10, 'cols': 100})
self.fields['requested_changes'].widget.attrs.update({
'placeholder': 'Give a numbered (1-, 2-, ...) list of specifically requested changes',
'cols': 100
})
self.fields['file_attachment'].label = 'File attachment (for a figure or similar)'
# Required fields on submission; optional on save as draft
if 'save_submit' in self.data:
required_fields = ['report', 'recommendation']
else:
required_fields = []
required_fields_label = ['report', 'recommendation']
for field in required_fields:
self.fields[field].required = True
# Let user know the field is required!
for field in required_fields_label:
self.fields[field].label += ' *'
if self.submission.eicrecommendations.active().exists():
# An active EICRecommendation is already formulated. This Report will be flagged.
self.report_type = REPORT_POST_EDREC
[docs] def save(self):
"""
Update meta data if ModelForm is submitted (non-draft).
Possibly overwrite the default status if user asks for saving as draft.
"""
report = super().save(commit=False)
report.report_type = self.report_type
report.submission = self.submission
report.date_submitted = timezone.now()
# Save with right status asked by user
if 'save_draft' in self.data:
report.status = STATUS_DRAFT
elif 'save_submit' in self.data:
report.status = STATUS_UNVETTED
# Update invitation and report meta data if exist
updated_invitations = self.submission.referee_invitations.filter(
referee=report.author).update(fulfilled=True)
if updated_invitations > 0:
report.invited = True
# Check if report author if the report is being flagged on the submission
if self.submission.referees_flagged:
if report.author.user.last_name in self.submission.referees_flagged:
report.flagged = True
report.save()
return report
[docs]class VetReportForm(forms.Form):
action_option = forms.ChoiceField(widget=forms.RadioSelect,
choices=REPORT_ACTION_CHOICES,
required=True, label='Action')
refusal_reason = forms.ChoiceField(choices=REPORT_REFUSAL_CHOICES, required=False)
email_response_field = forms.CharField(widget=forms.Textarea(),
label='Justification (optional)', required=False)
def __init__(self, *args, **kwargs):
self.report = kwargs.pop('report', None)
super().__init__(*args, **kwargs)
self.fields['email_response_field'].widget.attrs.update({
'placeholder': ('Optional: give a textual justification '
'(will be included in the email to the Report\'s author)'),
'rows': 5
})
[docs] def clean_refusal_reason(self):
"""Require a refusal reason if report is rejected."""
reason = self.cleaned_data['refusal_reason']
if self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE:
if not reason:
self.add_error('refusal_reason', 'A reason must be given to refuse a report.')
return reason
[docs] def process_vetting(self, current_contributor):
"""Set the right report status and update submission fields if needed."""
report = self.report
if self.cleaned_data['action_option'] == REPORT_ACTION_ACCEPT:
# Accept the report as is
Report.objects.filter(id=report.id).update(
status=STATUS_VETTED,
vetted_by=current_contributor,
)
report.submission.touch()
elif self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE:
# The report is rejected
Report.objects.filter(id=report.id).update(
status=self.cleaned_data['refusal_reason'],
)
else:
raise exceptions.InvalidReportVettingValue(self.cleaned_data['action_option'])
report.refresh_from_db()
return report
###################
# Communications #
###################
[docs]class EditorialCommunicationForm(forms.ModelForm):
[docs] class Meta:
model = EditorialCommunication
fields = ('text',)
widgets = {
'text': forms.Textarea(attrs={
'rows': 5,
'placeholder': 'Write your message in this box.'
}),
}
######################
# EIC Recommendation #
######################
[docs]class EICRecommendationForm(forms.ModelForm):
"""Formulate an EICRecommendation."""
DAYS_TO_VOTE = 7
assignment = None
earlier_recommendations = []
tier = forms.ChoiceField(
widget=forms.RadioSelect,
choices=SUBMISSION_TIERS,
required=False)
[docs] class Meta:
model = EICRecommendation
fields = [
'for_journal',
'recommendation',
'tier',
'remarks_for_authors',
'requested_changes',
'remarks_for_editorial_college'
]
widgets = {
'remarks_for_authors': forms.Textarea({
'placeholder': 'Your general remarks for the authors',
'rows': 10,
}),
'requested_changes': forms.Textarea({
'placeholder': ('If you request revisions, give a numbered (1-, 2-, ...)'
' list of specifically requested changes'),
}),
'remarks_for_editorial_college': forms.Textarea({
'placeholder': ('If you recommend to accept or refuse, the Editorial College '
'will vote; write any relevant remarks for the EC here.'),
}),
}
def __init__(self, *args, **kwargs):
"""Accept two additional kwargs.
-- submission: The Submission to formulate an EICRecommendation for.
-- reformulate (bool): Reformulate the currently available EICRecommendations.
"""
self.submission = kwargs.pop('submission')
self.reformulate = kwargs.pop('reformulate', False)
self.load_earlier_recommendations()
if self.reformulate:
latest_recommendation = self.earlier_recommendations.first()
if latest_recommendation:
kwargs['initial'] = {
'for_journal': latest_recommendation.for_journal,
'recommendation': latest_recommendation.recommendation,
}
super().__init__(*args, **kwargs)
self.fields['for_journal'].queryset = Journal.objects.active().filter(
Q(discipline=self.submission.discipline) | Q(name='SciPost Selections'))
self.fields['for_journal'].help_text=(
'Please be aware of all the points below!'
'<ul><li>SciPost Selections: means article in field flagship journal '
'(SciPost Physics, Astronomy, Biology, Chemistry...) '
'with extended abstract published separately in SciPost Selections. '
'Only choose this for '
'an <em>exceptionally</em> good submission to a flagship journal.</li>'
'<li>A submission to a flaghip which does not meet the latter\'s '
'tough expectations and criteria can be recommended for publication '
'in the field\'s Core journal (if it exists).</li>'
'<li>Conversely, an extremely good submission to a field\'s Core journal can be '
'recommended for publication in the field\'s flagship, provided '
'it fulfils the latter\'s expectations and criteria.</li>'
'</ul>'
)
self.fields['recommendation'].help_text=(
'Selecting any of the three Publish choices means that you recommend publication.<br>'
'Which one you choose simply indicates your ballpark evaluation of the '
'submission\'s quality and has no further consequence on the publication.'
)
self.load_assignment()
[docs] def clean(self):
cleaned_data = super().clean()
if cleaned_data['recommendation'] == EIC_REC_PUBLISH:
if not cleaned_data['for_journal']:
raise forms.ValidationError(
'If you recommend Publish, please specify for which Journal.')
if cleaned_data['tier'] == '':
raise forms.ValidationError(
'If you recommend Publish, please also provide a Tier.')
[docs] def save(self):
recommendation = super().save(commit=False)
recommendation.submission = self.submission
recommendation.voting_deadline += datetime.timedelta(days=self.DAYS_TO_VOTE) # Test this
recommendation.version = len(self.earlier_recommendations) + 1
# Delete any previous tierings (irrespective of new/updated recommendation):
SubmissionTiering.objects.filter(
submission=self.submission,
fellow=self.submission.editor_in_charge).delete()
if self.reformulate:
event_text = 'The Editorial Recommendation has been reformulated for Journal {}: {}.'
else:
event_text = 'An Editorial Recommendation has been formulated for Journal {}: {}.'
if recommendation.recommendation in [EIC_REC_MINOR_REVISION, EIC_REC_MAJOR_REVISION]:
# Minor/Major revision: return to Author; ask to resubmit
recommendation.status = DECISION_FIXED
Submission.objects.filter(id=self.submission.id).update(
open_for_reporting=False,
open_for_commenting=False,
reporting_deadline=timezone.now())
if self.assignment:
# The EIC has fulfilled this editorial assignment.
self.assignment.status = STATUS_COMPLETED
self.assignment.save()
# Add SubmissionEvents for both Author and EIC
self.submission.add_general_event(event_text.format(
str(recommendation.for_journal),
recommendation.get_recommendation_display()))
else:
# if rec is to publish, specify the tiering (deleting old ones first):
if recommendation.recommendation == EIC_REC_PUBLISH:
tiering = SubmissionTiering(
submission=self.submission,
fellow=self.submission.editor_in_charge,
for_journal=recommendation.for_journal,
tier=self.cleaned_data['tier'])
tiering.save()
# Add SubmissionEvent for EIC only
self.submission.add_event_for_eic(event_text.format(
str(recommendation.for_journal),
recommendation.get_recommendation_display()))
if self.earlier_recommendations:
self.earlier_recommendations.update(active=False, status=DEPRECATED)
# All reports already submitted are now formulated *after* eic rec formulation
Report.objects.filter(
submission__eicrecommendations__in=self.earlier_recommendations).update(
report_type=REPORT_NORMAL)
recommendation.save()
return recommendation
[docs] def revision_requested(self):
return self.instance.recommendation in [EIC_REC_MINOR_REVISION, EIC_REC_MAJOR_REVISION]
[docs] def load_assignment(self):
# Find EditorialAssignment for Submission
try:
self.assignment = self.submission.editorial_assignments.accepted().get(
to=self.submission.editor_in_charge)
return True
except EditorialAssignment.DoesNotExist:
return False
[docs] def load_earlier_recommendations(self):
"""Load and save EICRecommendations related to Submission of the instance."""
self.earlier_recommendations = self.submission.eicrecommendations.all()
###############
# Vote form #
###############
[docs]class RecommendationVoteForm(forms.Form):
"""Cast vote on EICRecommendation form."""
vote = forms.ChoiceField(
widget=forms.RadioSelect, choices=[
('agree', 'Agree'), ('disagree', 'Disagree'), ('abstain', 'Abstain')])
tier = forms.ChoiceField(
widget=forms.RadioSelect,
choices=SUBMISSION_TIERS,
required=False)
alternative_for_journal = forms.ModelChoiceField(
label='Alternative recommendation: for which Journal?',
widget=forms.Select,
queryset=Journal.objects.active(),
required=False
)
alternative_recommendation = forms.ChoiceField(
label='Which action do you recommend?',
widget=forms.Select, choices=ALT_REC_CHOICES,
required=False)
remark = forms.CharField(widget=forms.Textarea(attrs={
'rows': 3,
'cols': 30,
'placeholder': 'Any further remark you want to add? (optional)'
}), label='', required=False)
[docs] def clean(self):
cleaned_data = super().clean()
if (cleaned_data['vote'] == 'disagree' and (
cleaned_data['alternative_for_journal'] is None or
cleaned_data['alternative_recommendation'] == '')):
raise forms.ValidationError(
'If you disagree, you must provide an alternative recommendation '
'(by filling both the for journal and recommendation fields).')
[docs]class EditorialDecisionForm(forms.ModelForm):
"""For EdAdmin to fix the outcome on a Submission, after voting is completed."""
[docs] class Meta:
model = EditorialDecision
fields = [
'submission',
'for_journal',
'decision',
'taken_on',
'remarks_for_authors',
'remarks_for_editorial_college',
'status'
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.id:
self.fields['submission'].queryset = Submission.objects.filter(
pk=self.instance.submission.id)
else:
self.fields['submission'].queryset = Submission.objects.actively_refereeing()
self.fields['remarks_for_authors'].widget.attrs.update({
'placeholder': '[will be seen by authors and Fellows]'})
self.fields['remarks_for_editorial_college'].widget.attrs.update({
'placeholder': '[will only be seen by Fellows]'})
[docs] def clean(self):
cleaned_data = super().clean()
if (cleaned_data['decision'] == EIC_REC_REJECT and
cleaned_data['status'] == EditorialDecision.AWAITING_PUBOFFER_ACCEPTANCE):
raise forms.ValidationError(
'If the decision is to reject, the status cannot be '
'Awaiting author acceptance of publication offer.')
[docs] def save(self):
decision = super().save(commit=False)
if not self.instance.id: # a new object is created
if self.cleaned_data['submission'].editorialdecision_set.all().exists():
decision.version = self.cleaned_data['submission'].editorialdecision_set.all(
).last().version + 1
decision.save()
return decision
[docs]class RestartRefereeingForm(forms.Form):
"""
For EdAdmin to restart the latest refereeing round.
"""
confirm = forms.ChoiceField(
widget=forms.RadioSelect, choices=((True, 'Confirm'), (False, 'Abort')), label='')
def __init__(self, *args, **kwargs):
"""Add related submission as argument."""
self.submission = kwargs.pop('submission')
super().__init__(*args, **kwargs)
[docs] def save(self):
if self.is_confirmed():
Submission.objects.filter(id=self.submission.id).update(
status=STATUS_EIC_ASSIGNED,
refereeing_cycle=CYCLE_UNDETERMINED,
acceptance_date=None,
latest_activity=timezone.now())
self.submission.editorial_assignments.filter(
to=self.submission.editor_in_charge,
status=STATUS_COMPLETED
).update(status=STATUS_ACCEPTED)
self.submission.eicrecommendations.active().update(status=DEPRECATED)
# Delete any production stream
if hasattr(self.submission, 'production_stream'):
self.submission.production_stream.delete()
self.submission.refresh_from_db()
return self.submission
[docs]class SubmissionCycleChoiceForm(forms.ModelForm):
"""Make a decision on the Submission's cycle and make publicly available."""
referees_reinvite = forms.ModelMultipleChoiceField(
queryset=RefereeInvitation.objects.none(),
widget=forms.CheckboxSelectMultiple({'checked': 'checked'}),
required=False, label='Reinvite referees')
[docs] class Meta:
model = Submission
fields = ('refereeing_cycle',)
widgets = {'refereeing_cycle': forms.RadioSelect}
def __init__(self, *args, **kwargs):
"""Update choices and queryset."""
super().__init__(*args, **kwargs)
self.fields['refereeing_cycle'].choices = SUBMISSION_CYCLE_CHOICES
other_submissions = self.instance.other_versions.all()
if other_submissions:
self.fields['referees_reinvite'].queryset = RefereeInvitation.objects.filter(
submission__in=other_submissions).distinct()
[docs] def save(self):
"""Make Submission publicly available after decision."""
self.instance.visible_public = True
return super().save()
[docs]class iThenticateReportForm(forms.ModelForm):
def __init__(self, submission, *args, **kwargs):
self.submission = submission
super().__init__(*args, **kwargs)
if kwargs.get('files', {}).get('file'):
# Add file field if file data is coming in!
self.fields['file'] = forms.FileField()
[docs] def clean(self):
cleaned_data = super().clean()
doc_id = self.instance.doc_id
if not doc_id and not self.fields.get('file'):
try:
cleaned_data['document'] = helpers.retrieve_pdf_from_arxiv(
self.submission.preprint.identifier_w_vn_nr)
except exceptions.ArxivPDFNotFound:
self.add_error(
None, 'The pdf could not be found at arXiv. Please upload the pdf manually.')
self.fields['file'] = forms.FileField()
elif not doc_id and cleaned_data.get('file'):
cleaned_data['document'] = cleaned_data['file'].read()
elif doc_id:
self.document_id = doc_id
# Login client to append login-check to form
self.client = self.get_client()
if not self.client:
return None
# Document (id) is found
if cleaned_data.get('document'):
self.document = cleaned_data['document']
try:
self.response = self.call_ithenticate()
except AttributeError:
if not self.fields.get('file'):
# The document is invalid.
self.add_error(None, ('A valid pdf could not be found at arXiv.'
' Please upload the pdf manually.'))
else:
self.add_error(None, ('The uploaded file is not valid.'
' Please upload a valid pdf.'))
self.fields['file'] = forms.FileField()
elif hasattr(self, 'document_id'):
self.response = self.call_ithenticate()
if hasattr(self, 'response') and self.response:
return cleaned_data
# Don't return anything as someone submitted invalid data for the form at this point!
return None
[docs] def save(self, *args, **kwargs):
data = self.response
report, created = iThenticateReport.objects.get_or_create(doc_id=data['id'])
if not created:
try:
iThenticateReport.objects.filter(doc_id=data['id']).update(
uploaded_time=data['uploaded_time'],
processed_time=data['processed_time'],
percent_match=data['percent_match'],
part_id=data.get('parts', [{}])[0].get('id')
)
except KeyError:
pass
else:
report.save()
Submission.objects.filter(id=self.submission.id).update(plagiarism_report=report)
return report
[docs] def call_ithenticate(self):
if hasattr(self, 'document_id'):
# Update iThenticate status
return self.update_status()
elif hasattr(self, 'document'):
# Upload iThenticate document first time
return self.upload_document()
[docs] def get_client(self):
client = iThenticate.API.Client(settings.ITHENTICATE_USERNAME,
settings.ITHENTICATE_PASSWORD)
if client.login():
return client
self.add_error(None, "Failed to login to iThenticate.")
return None
[docs] def update_status(self):
client = self.client
response = client.documents.get(self.document_id)
if response['status'] == 200:
return response.get('data')[0].get('documents')[0]
self.add_error(None, "Updating failed. iThenticate didn't return valid data [1]")
for msg in client.messages:
self.add_error(None, msg)
return None
[docs] def upload_document(self):
from .plagiarism import iThenticate
plagiarism = iThenticate()
data = plagiarism.upload_submission(self.document, self.submission)
# Give feedback to the user
if not data:
self.add_error(None, "Updating failed. iThenticate didn't return valid data [3]")
for msg in plagiarism.get_messages():
self.add_error(None, msg)
return None
return data