Newer
Older
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import re
from django import forms
from django.conf import settings
# from django.contrib.postgres.search import TrigramSimilarity
from django.db import transaction
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, REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3, STATUS_VETTED,
REPORT_MINOR_REV, REPORT_MAJOR_REV, REPORT_REJECT, 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,
iThenticateReport, EditorialCommunication)
from journals.models import Journal
from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS
from preprints.helpers import generate_new_scipost_identifier
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}$'
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]))
def search_results(self):
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', '')
)
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)
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')
def status_verbose(self):
try:
return dict(SUBMISSION_STATUS)[self.cleaned_data['status']]
except KeyError:
return ''
###############################
# Submission and resubmission #
###############################
class SubmissionService:
"""
Object to run checks for prefiller and submit manuscript forms.
"""
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
@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
@property
def arxiv_data(self):
if self._arxiv_data is None:
self._call_arxiv()
return self._arxiv_data
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)
else:
error_message = 'A preprint associated to this identifier does not exist.'
raise forms.ValidationError(error_message)
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,
'domain': self.latest_submission.domain,
'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 {}
def is_resubmission(self):
"""
Check if Submission is a SciPost or arXiv resubmission.
"""
return self.latest_submission is not None
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)
# No match object returned, identifier is invalid
error_message = ('The journal you want to submit to does not allow for this'
raise forms.ValidationError(error_message, code='submitted_to')
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,
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)
"""
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.
"""
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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)
"""
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})
class SubmissionForm(forms.ModelForm):
"""
Form to submit a new (re)Submission.
"""
identifier_w_vn_nr = forms.CharField(widget=forms.HiddenInput())
preprint_file = forms.FileField()
class Meta:
model = Submission
fields = [
'submitted_to',
'submission_type',
'domain',
'subject_area',
'secondary_areas',
'title',
'author_list',
'abstract',
'author_comments',
'list_of_changes',
'remarks_for_editors',
'referees_suggested',
'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,
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()
self.fields['submitted_to'].label = 'Journal: submit to'
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=SCIPOST_JOURNAL_PHYSICS_PROC)
def is_resubmission(self):
return self.service.is_resubmission()
def clean(self, *args, **kwargs):
cleaned_data = super().clean(*args, **kwargs)
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 self.cleaned_data['submitted_to'].doi_label != SCIPOST_JOURNAL_PHYSICS_PROC:
try:
del self.cleaned_data['proceedings']
except KeyError:
# No proceedings returned to data
return cleaned_data
return cleaned_data
def clean_author_list(self):
"""
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.')
"""
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 == SCIPOST_JOURNAL_PHYSICS and not submission_type:
self.add_error('submission_type', 'Please specify the submission type.')
return submission_type
"""
Set the default set of (guest) Fellows for this Submission.
"""
fellows = qs.regular().filter(
contributor__discipline=submission.discipline).return_active_for_submission(submission)
# Add Guest Fellowships if the Submission is a Proceedings manuscript
guest_fellows = qs.guests().filter(
proceedings=submission.proceedings).return_active_for_submission(submission)
@transaction.atomic
def save(self):
"""
submission = super().save(commit=False)
submission.submitted_by = self.requested_by.contributor
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
self.service.process_resubmission_procedure(submission)
submission.authors.add(self.requested_by.contributor)
# Return latest version of the Submission. It could be outdated by now.
submission.refresh_from_db()
return submission
class SubmissionIdentifierForm(forms.Form):
"""
Prefill SubmissionForm using this form that takes an arXiv ID only.
"""
IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)'
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)
"""
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()
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']
'discipline': self.service.latest_submission.discipline,
'domain': self.service.latest_submission.domain,
'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
class SubmissionReportsForm(forms.ModelForm):
class Meta:
model = Submission
fields = ['pdf_refereeing_pack']
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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())
class Meta:
model = EditorialAssignment
fields = ('to',)
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
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
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)
class BasePreassignEditorsFormSet(forms.BaseModelFormSet):
"""Preassign editors for incoming Submission."""
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
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)
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()
def get_form_kwargs(self, index):
"""Add submission to form arguments."""
kwargs = super().get_form_kwargs(index)
kwargs['submission'] = self.submission
return kwargs
def save(self, commit=True):
"""Save each form and order EditorialAssignments."""
objects = super().save(commit=False)
objects = []
count = 0
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)
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
class SubmissionReassignmentForm(forms.ModelForm):
"""Process reassignment of EIC for Submission."""
new_editor = forms.ModelChoiceField(queryset=Contributor.objects.none(), required=True)
class Meta:
model = Submission
fields = ()
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)
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(
mail_code='fellows/email_fellow_replaced_by_other',
assignment=old_assignment)
mail_sender.send()
mail_sender = DirectMailUtil(
mail_code='fellows/email_fellow_assigned_submission',
assignment=assignment)
mail_sender.send()
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'}))
class Meta:
model = Submission
fields = ()
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)
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
@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
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
class WithdrawSubmissionForm(forms.ModelForm):
"""
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)
def is_confirmed(self):
return self.cleaned_data.get('confirm') in (True, 'True')
def save(self):
if self.is_confirmed():
# Update submission (current + any previous versions)
Submission.objects.filter(id=self.instance.id).update(
visible_public=False, visible_pool=False,
open_for_commenting=False, open_for_reporting=False,
status=STATUS_WITHDRAWN, latest_activity=timezone.now())
self.instance.get_other_versions().update(visible_public=False)
# Update all assignments
EditorialAssignment.objects.filter(submission=self.instance).need_response().update(
status=STATUS_DEPRECATED)
EditorialAssignment.objects.filter(submission=self.instance).accepted().update(
status=STATUS_COMPLETED)
# Deprecate any outstanding recommendations
EICRecommendation.objects.filter(submission=self.instance).active().update(
status=DEPRECATED)
self.instance.refresh_from_db()
return self.instance
######################
# Editorial workflow #
######################
class InviteEditorialAssignmentForm(forms.ModelForm):
"""Invite new Fellow; create EditorialAssignment for Submission."""
class Meta:
model = EditorialAssignment
fields = ('to',)
labels = {
'to': 'Fellow',
}
def __init__(self, *args, **kwargs):
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')
def save(self, commit=True):
self.instance.submission = self.submission
return super().save(commit)
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)
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')
super().__init__(*args, **kwargs)
if not self.instance.id:
del self.fields['decision']
del self.fields['refusal_reason']
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'
def is_normal_cycle(self):
"""Check if normal refereeing cycle is chosen."""
return self.cleaned_data['refereeing_cycle'] == CYCLE_DEFAULT
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.
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())
else:
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())
# Implicitly or explicity accept the assignment and deprecate others.
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)
assignment.status = STATUS_DECLINED
assignment.refusal_reason = self.cleaned_data['refusal_reason']
assignment.save() # Save again to register acceptance
return assignment
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)
class RefereeSearchForm(forms.Form):
'placeholder': 'Search for a referee in the SciPost Profiles database'}))
return Profile.objects.filter(
last_name__icontains=self.cleaned_data['last_name'])
# return Profile.objects.annotate(
# similarity=TrigramSimilarity('last_name', self.cleaned_data['last_name']),
# ).filter(similarity__gt=0.3).order_by('-similarity')
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)
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"),
))
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')
class VotingEligibilityForm(forms.ModelForm):
"""Assign Fellows to vote for EICRecommendation and open its status for voting."""
eligible_fellows = forms.ModelMultipleChoiceField(
widget=forms.CheckboxSelectMultiple(),
required=True, label='Eligible for voting')
class Meta:
model = EICRecommendation
fields = ()
def __init__(self, *args, **kwargs):
"""Get queryset of Contributors eligible for voting."""
secondary_areas = self.instance.submission.secondary_areas
if not secondary_areas:
secondary_areas = []
self.fields['eligible_fellows'].queryset = Contributor.objects.filter(
Q(expertises__contains=[self.instance.submission.subject_area]) |