SciPost Code Repository

Skip to content
Snippets Groups Projects
forms.py 63.3 KiB
Newer Older
__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
import datetime
import re

from django import forms
from django.conf import settings
# from django.contrib.postgres.search import TrigramSimilarity
from django.db import transaction
Jorran de Wit's avatar
Jorran de Wit committed
from django.db.models import Q
from django.forms.formsets import ORDERING_FIELD_NAME
from django.utils import timezone

Jorran de Wit's avatar
Jorran de Wit committed
from .constants import (
    ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED, REPORT_ACTION_CHOICES,
Jorran de Wit's avatar
Jorran de Wit committed
    REPORT_REFUSAL_CHOICES, STATUS_REJECTED, STATUS_INCOMING, REPORT_POST_EDREC, REPORT_NORMAL,
Jorran de Wit's avatar
Jorran de Wit committed
    STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE, STATUS_UNASSIGNED,
Jorran de Wit's avatar
3.  
Jorran de Wit committed
    EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS, PUT_TO_VOTING, CYCLE_UNDETERMINED,
Jorran de Wit's avatar
Jorran de Wit committed
    SUBMISSION_CYCLE_CHOICES, REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3, STATUS_VETTED,
Jorran de Wit's avatar
Jorran de Wit committed
    REPORT_MINOR_REV, REPORT_MAJOR_REV, REPORT_REJECT, DECISION_FIXED, DEPRECATED, STATUS_COMPLETED,
Jorran de Wit's avatar
Jorran de Wit committed
    STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC, STATUS_PREASSIGNED, STATUS_REPLACED,
Jorran de Wit's avatar
Jorran de Wit committed
    STATUS_FAILED_PRESCREENING, STATUS_DEPRECATED, STATUS_ACCEPTED, STATUS_DECLINED, STATUS_WITHDRAWN)
from . import exceptions, helpers
from .helpers import to_ascii_only
Jorran de Wit's avatar
Jorran de Wit committed
from .models import (
    Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment,
    iThenticateReport, EditorialCommunication)
Jorran de Wit's avatar
3.  
Jorran de Wit committed
from .signals import notify_manuscript_accepted
Jorran de Wit's avatar
Jorran de Wit committed
from colleges.models import Fellowship
Jorran de Wit's avatar
Jorran de Wit committed
from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS
Jorran de Wit's avatar
Jorran de Wit committed
from mails.utils import DirectMailUtil
from preprints.helpers import generate_new_scipost_identifier
from preprints.models import Preprint
Jorran de Wit's avatar
3.  
Jorran de Wit committed
from production.utils import get_or_create_production_stream
Jorran de Wit's avatar
Jorran de Wit committed
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

Jorran de Wit's avatar
Jorran de Wit committed
IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}\.[0-9]{4,5}v[0-9]{1,2}$'


class SubmissionSearchForm(forms.Form):
Jorran de Wit's avatar
Jorran de Wit committed
    """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):
Jorran de Wit's avatar
Jorran de Wit committed
        """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', '')
        )


Jorran de Wit's avatar
Jorran de Wit committed
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)
Jorran de Wit's avatar
Jorran de Wit committed

    def search(self, queryset, current_user):
Jorran de Wit's avatar
Jorran de Wit committed
        if self.cleaned_data.get('status'):
            # Do extra check on non-required field to never show errors on template
Jorran de Wit's avatar
Jorran de Wit committed
            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')
Jorran de Wit's avatar
Jorran de Wit committed

Jorran de Wit's avatar
Jorran de Wit committed
    def status_verbose(self):
        try:
            return dict(SUBMISSION_STATUS)[self.cleaned_data['status']]
        except KeyError:
            return ''

Jorran de Wit's avatar
Jorran de Wit committed

###############################
# Submission and resubmission #
###############################

Jorran de Wit's avatar
Jorran de Wit committed
class SubmissionService:
    """
    Object to run checks for prefiller and submit manuscript forms.
    """
Jorran de Wit's avatar
Jorran de Wit committed

Jorran de Wit's avatar
Jorran de Wit committed
    metadata = {}
Jorran de Wit's avatar
Jorran de Wit committed
    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
Jorran de Wit's avatar
Jorran de Wit committed
        self._arxiv_data = None
Jorran de Wit's avatar
Jorran de Wit committed

    @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
Jorran de Wit's avatar
Jorran de Wit committed
        return self._latest_submission

Jorran de Wit's avatar
Jorran de Wit committed
    @property
    def arxiv_data(self):
        if self._arxiv_data is None:
            self._call_arxiv()
        return self._arxiv_data

Jorran de Wit's avatar
Jorran de Wit committed
    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()

Jorran de Wit's avatar
Jorran de Wit committed
    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:
Jorran de Wit's avatar
Jorran de Wit committed
            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)

Jorran de Wit's avatar
Jorran de Wit committed
    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 {}
Jorran de Wit's avatar
Jorran de Wit committed
    def is_resubmission(self):
        """
        Check if Submission is a SciPost or arXiv resubmission.
        """
        return self.latest_submission is not None
Jorran de Wit's avatar
Jorran de Wit committed
    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)
Jorran de Wit's avatar
Jorran de Wit committed
        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'
Jorran de Wit's avatar
Jorran de Wit committed
                             ' identifier. Please contact SciPost if you have'
                             ' any further questions.')
            raise forms.ValidationError(error_message, code='submitted_to')
Jorran de Wit's avatar
Jorran de Wit committed
    def process_resubmission_procedure(self, submission):
        """
        Update all fields for new and old Submission and EditorialAssignments to comply with
        the resubmission procedures.
Jorran de Wit's avatar
Jorran de Wit committed
        -- submission: the new version of the Submission series.
        """
        if not self.latest_submission:
            raise Submission.DoesNotExist
Jorran de Wit's avatar
Jorran de Wit committed
        # Close last submission
        Submission.objects.filter(id=self.latest_submission.id).update(
            is_current=False, open_for_reporting=False, status=STATUS_RESUBMITTED)
Jorran de Wit's avatar
Jorran de Wit committed
        # Copy Topics
        submission.topics.add(*self.latest_submission.topics.all())
Jorran de Wit's avatar
Jorran de Wit committed
        # Open for comment and reporting and copy EIC info
        Submission.objects.filter(id=submission.id).update(
            open_for_reporting=True,
            open_for_commenting=True,
Jorran de Wit's avatar
Jorran de Wit committed
            is_resubmission_of=self.latest_submission,
Jorran de Wit's avatar
Jorran de Wit committed
            visible_pool=True,
            refereeing_cycle=CYCLE_UNDETERMINED,
            editor_in_charge=self.latest_submission.editor_in_charge,
Jorran de Wit's avatar
Jorran de Wit committed
            status=STATUS_EIC_ASSIGNED,
            thread_hash=self.latest_submission.thread_hash)
Jorran de Wit's avatar
Jorran de Wit committed

Jorran de Wit's avatar
Jorran de Wit committed
        # 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())
Jorran de Wit's avatar
Jorran de Wit committed
        # Create new EditorialAssigment for the current Editor-in-Charge
        EditorialAssignment.objects.create(
            submission=submission,
            to=self.latest_submission.editor_in_charge,
            status=STATUS_ACCEPTED)
Jorran de Wit's avatar
Jorran de Wit committed
    def _submission_already_exists(self):
        """
        Check if preprint has already been submitted before.
        """
Jorran de Wit's avatar
Jorran de Wit committed
        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')

Jorran de Wit's avatar
Jorran de Wit committed
    def _submission_previous_version_is_valid_for_submission(self):
        """
        Check if previous submitted versions have the appropriate status.
        """
Jorran de Wit's avatar
Jorran de Wit committed
        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)
Jorran de Wit's avatar
Jorran de Wit committed
    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})

Jorran de Wit's avatar
Jorran de Wit committed

Jorran de Wit's avatar
Jorran de Wit committed
class SubmissionForm(forms.ModelForm):
    """
    Form to submit a new (re)Submission.
    """
    identifier_w_vn_nr = forms.CharField(widget=forms.HiddenInput())
    preprint_file = forms.FileField()
Jorran de Wit's avatar
Jorran de Wit committed

    class Meta:
        model = Submission
        fields = [
Jorran de Wit's avatar
Jorran de Wit committed
            'is_resubmission_of',
Jorran de Wit's avatar
Jorran de Wit committed
            'proceedings',
            'submission_type',
            'domain',
            'subject_area',
            'secondary_areas',
            'title',
            'author_list',
            'abstract',
            'author_comments',
            'list_of_changes',
            'remarks_for_editors',
            'referees_suggested',
Jorran de Wit's avatar
Jorran de Wit committed
            'referees_flagged',
            'arxiv_link',
Jorran de Wit's avatar
Jorran de Wit committed
            'is_resubmission_of': forms.HiddenInput(),
Jorran de Wit's avatar
Jorran de Wit committed
            'secondary_areas': forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS),
Jorran de Wit's avatar
Jorran de Wit committed
            '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):
Jorran de Wit's avatar
Jorran de Wit committed
        self.requested_by = kwargs.pop('requested_by')
        self.preprint_server = kwargs.pop('preprint_server', 'arxiv')
        self.resubmission_preprint = kwargs['initial'].get('resubmission', False)

Jorran de Wit's avatar
Jorran de Wit committed
        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')

Jorran de Wit's avatar
Jorran de Wit committed
        self.service = SubmissionService(
            self.requested_by, self.preprint_server,
Jorran de Wit's avatar
Jorran de Wit committed
            identifier=identifier,
Jorran de Wit's avatar
Jorran de Wit committed
            resubmission_of_id=self.resubmission_preprint)
        if self.preprint_server == 'scipost':
            kwargs['initial'] = self.service.get_latest_submission_data()
        super().__init__(*args, **kwargs)

Jorran de Wit's avatar
Jorran de Wit committed
        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']

Jorran de Wit's avatar
Jorran de Wit committed
        if not self.fields['is_resubmission_of'].initial:
            # No intial nor submitted data found.
            del self.fields['is_resubmission_of']
Jorran de Wit's avatar
Jorran de Wit committed
        # Select Journal instances.
        self.fields['submitted_to'].queryset = Journal.objects.active()
        self.fields['submitted_to'].label = 'Journal: submit to'

Jorran de Wit's avatar
Jorran de Wit committed
        # Proceedings submission fields
Jorran de Wit's avatar
Jorran de Wit committed
        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
Jorran de Wit's avatar
Jorran de Wit committed
            self.fields['submitted_to'].queryset = self.fields['submitted_to'].queryset.exclude(
                doi_label=SCIPOST_JOURNAL_PHYSICS_PROC)
            del self.fields['proceedings']
Jorran de Wit's avatar
Jorran de Wit committed
    def is_resubmission(self):
        return self.service.is_resubmission()

    def clean(self, *args, **kwargs):
Jorran de Wit's avatar
Jorran de Wit committed
        Do all general checks for Submission.
        cleaned_data = super().clean(*args, **kwargs)

Jorran de Wit's avatar
Jorran de Wit committed
        # SciPost preprints are auto-generated here.
Jorran de Wit's avatar
Jorran de Wit committed
        self.scipost_identifier = None
        if 'identifier_w_vn_nr' not in cleaned_data:
Jorran de Wit's avatar
Jorran de Wit committed
            self.service.identifier, self.scipost_identifier = generate_new_scipost_identifier(
                cleaned_data.get('is_resubmission_of', None))
Jorran de Wit's avatar
Jorran de Wit committed
            # Also copy to the form data
            self.cleaned_data['identifier_w_vn_nr'] = self.service.identifier
Jorran de Wit's avatar
Jorran de Wit committed
        # 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:
Jorran de Wit's avatar
Jorran de Wit committed
            try:
                del self.cleaned_data['proceedings']
            except KeyError:
                # No proceedings returned to data
                return cleaned_data
        return cleaned_data

    def clean_author_list(self):
Jorran de Wit's avatar
Jorran de Wit committed
        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.')
Jorran de Wit's avatar
Jorran de Wit committed
            self.add_error('author_list', error_message)
Jorran de Wit's avatar
Jorran de Wit committed
    def clean_submission_type(self):
Jorran de Wit's avatar
Jorran de Wit committed
        """
        Validate Submission type for the SciPost Physics journal.
Jorran de Wit's avatar
Jorran de Wit committed
        """
        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:
Jorran de Wit's avatar
Jorran de Wit committed
            self.add_error('submission_type', 'Please specify the submission type.')
        return submission_type

Jorran de Wit's avatar
Jorran de Wit committed
    def set_pool(self, submission):
Jorran de Wit's avatar
Jorran de Wit committed
        """
        Set the default set of (guest) Fellows for this Submission.
        """
Jorran de Wit's avatar
Jorran de Wit committed
        qs = Fellowship.objects.active()
Jorran de Wit's avatar
Jorran de Wit committed
        fellows = qs.regular().filter(
            contributor__discipline=submission.discipline).return_active_for_submission(submission)
Jorran de Wit's avatar
Jorran de Wit committed
        submission.fellows.set(fellows)

Jorran de Wit's avatar
Jorran de Wit committed
        if submission.proceedings:
Jorran de Wit's avatar
Jorran de Wit committed
            # Add Guest Fellowships if the Submission is a Proceedings manuscript
            guest_fellows = qs.guests().filter(
Jorran de Wit's avatar
Jorran de Wit committed
                proceedings=submission.proceedings).return_active_for_submission(submission)
            submission.fellows.add(*guest_fellows)
    @transaction.atomic
    def save(self):
Jorran de Wit's avatar
Jorran de Wit committed
        """
        Create the new Submission and Preprint instances.
        """
        submission = super().save(commit=False)
        submission.submitted_by = self.requested_by.contributor

        # Save identifiers
Jorran de Wit's avatar
Jorran de Wit committed
        identifiers = self.cleaned_data['identifier_w_vn_nr'].rpartition('v')
        preprint, __ = Preprint.objects.get_or_create(
Jorran de Wit's avatar
Jorran de Wit committed
            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
Jorran de Wit's avatar
Jorran de Wit committed
        submission.metadata = self.service.metadata
        submission.preprint = preprint
Jorran de Wit's avatar
Jorran de Wit committed
        submission.save()
        if self.is_resubmission():
Jorran de Wit's avatar
Jorran de Wit committed
            self.service.process_resubmission_procedure(submission)
Jorran de Wit's avatar
Jorran de Wit committed

        # Gather first known author and Fellows.
        submission.authors.add(self.requested_by.contributor)
Jorran de Wit's avatar
Jorran de Wit committed
        self.set_pool(submission)

        # Return latest version of the Submission. It could be outdated by now.
Jorran de Wit's avatar
Jorran de Wit committed
        submission.refresh_from_db()
        return submission


class SubmissionIdentifierForm(forms.Form):
    """
    Prefill SubmissionForm using this form that takes an arXiv ID only.
    """
Jorran de Wit's avatar
Jorran de Wit committed

    IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)'

    identifier_w_vn_nr = forms.RegexField(
Jorran de Wit's avatar
Jorran de Wit committed
        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}))
Jorran de Wit's avatar
Jorran de Wit committed
    def __init__(self, *args, **kwargs):
        self.requested_by = kwargs.pop('requested_by')
        return super().__init__(*args, **kwargs)

Jorran de Wit's avatar
Jorran de Wit committed

    def clean_identifier_w_vn_nr(self):
Jorran de Wit's avatar
Jorran de Wit committed
        """
        Do basic prechecks based on the arXiv ID only.
        """
Jorran de Wit's avatar
Jorran de Wit committed
        identifier = self.cleaned_data.get('identifier_w_vn_nr', None)

Jorran de Wit's avatar
Jorran de Wit committed
        self.service = SubmissionService(self.requested_by, 'arxiv', identifier=identifier)
        self.service.run_checks()
Jorran de Wit's avatar
Jorran de Wit committed
    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']
Jorran de Wit's avatar
Jorran de Wit committed
        if self.service.is_resubmission():
            form_data.update({
Jorran de Wit's avatar
Jorran de Wit committed
                '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):
Jorran de Wit's avatar
Jorran de Wit committed
    """Update refereeing pdf for Submission."""

    class Meta:
        model = Submission
        fields = ['pdf_refereeing_pack']


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)


Jorran de Wit's avatar
5.  
Jorran de Wit committed
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)

    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
Jorran de Wit's avatar
5.  
Jorran de Wit committed
        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)
Jorran de Wit's avatar
5.  
Jorran de Wit committed
        return objects


PreassignEditorsFormSet = forms.modelformset_factory(
    EditorialAssignment, can_order=True, extra=0,
    formset=BasePreassignEditorsFormSet, form=PreassignEditorsForm)
Jorran de Wit's avatar
5.  
Jorran de Wit committed


Jorran de Wit's avatar
Jorran de Wit committed
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()


Jorran de Wit's avatar
Jorran de Wit committed
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.'))
Jorran de Wit's avatar
Jorran de Wit committed
    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'}))

Jorran de Wit's avatar
Jorran de Wit committed
    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')
Jorran de Wit's avatar
Jorran de Wit committed
        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.')
Jorran de Wit's avatar
Jorran de Wit committed
        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

Jorran de Wit's avatar
Jorran de Wit committed
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."""
Jorran de Wit's avatar
Jorran de Wit committed

    class Meta:
        model = EditorialAssignment
        fields = ('to',)
        labels = {
            'to': 'Fellow',
        }

    def __init__(self, *args, **kwargs):
Jorran de Wit's avatar
Jorran de Wit committed
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)
        self.fields['to'].queryset = Contributor.objects.available().filter(
Jorran de Wit's avatar
Jorran de Wit committed
            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')
        self.request = kwargs.pop('request')
        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.
        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())
            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())
            # Implicitly or explicity accept the assignment and deprecate others.
Jorran de Wit's avatar
Jorran de Wit committed
            # 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)
Jorran de Wit's avatar
Jorran de Wit committed
            # 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
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):
Jorran de Wit's avatar
Jorran de Wit committed
    last_name = forms.CharField(widget=forms.TextInput({
        'placeholder': 'Search for a referee in the SciPost Profiles database'}))
Jorran de Wit's avatar
Jorran de Wit committed
    def search(self):
        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):
Jorran de Wit's avatar
Jorran de Wit committed
    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):
Jorran de Wit's avatar
3.  
Jorran de Wit committed
    """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')

    class Meta:
        model = EICRecommendation
        fields = ()

    def __init__(self, *args, **kwargs):
        """Get queryset of Contributors eligible for voting."""
        super().__init__(*args, **kwargs)
Jorran de Wit's avatar
Jorran de Wit committed
        secondary_areas = self.instance.submission.secondary_areas
        if not secondary_areas:
            secondary_areas = []

        self.fields['eligible_fellows'].queryset = Contributor.objects.filter(
Jorran de Wit's avatar
Jorran de Wit committed
            fellowships__pool=self.instance.submission).filter(
Jorran de Wit's avatar
Jorran de Wit committed
                Q(EIC=self.instance.submission) |
Jorran de Wit's avatar
Jorran de Wit committed
                Q(expertises__contains=[self.instance.submission.subject_area]) |