__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)" __license__ = "AGPL v3" from .appraisal import QualificationForm, ReadinessForm import datetime from django import forms from django.conf import settings from django.db import transaction from django.db.models import Q, Count from django.forms.formsets import ORDERING_FIELD_NAME from django.utils import timezone from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit from crispy_bootstrap5.bootstrap5 import FloatingField from dal import autocomplete from ..constants import ( REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES, REPORT_POST_EDREC, REPORT_NORMAL, STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE, PUT_TO_VOTING, SUBMISSION_CYCLE_CHOICES, CYCLE_UNDETERMINED, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC, EIC_REC_PUBLISH, EIC_REC_MINOR_REVISION, EIC_REC_MAJOR_REVISION, EIC_REC_REJECT, ALT_REC_CHOICES, SUBMISSION_TIERS, STATUS_VETTED, DECISION_FIXED, DEPRECATED, ) from .. import exceptions, helpers from ..helpers import to_ascii_only from ..models import ( PreprintServer, SubmissionAuthorProfile, Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment, SubmissionTiering, EditorialDecision, PlagiarismAssessment, iThenticateReport, EditorialCommunication, ) from ..regexes import CHEMRXIV_DOI_PATTERN from colleges.models import Fellowship from common.utils import Q_with_alternative_spellings from journals.models import Journal, Publication from journals.constants import ( PUBLISHABLE_OBJECT_TYPE_ARTICLE, PUBLISHABLE_OBJECT_TYPE_CODEBASE, PUBLISHABLE_OBJECT_TYPE_DATASET, ) from mails.utils import DirectMailUtil from ontology.models import AcademicField, Specialty, Topic from preprints.helpers import get_new_scipost_identifier from preprints.models import Preprint from proceedings.models import Proceedings from profiles.models import Profile from scipost.services import DOICaller, ArxivCaller, FigshareCaller, OSFPreprintsCaller from scipost.models import Contributor, Remark import strings import iThenticate ARXIV_IDENTIFIER_PATTERN_NEW = r"^[0-9]{4,}\.[0-9]{4,5}v[0-9]{1,2}$" FIGSHARE_IDENTIFIER_PATTERN = r"^[0-9]+\.v[0-9]{1,2}$" OSFPREPRINTS_IDENTIFIER_PATTERN = r"^[a-z0-9]+$" class PortalSubmissionSearchForm(forms.Form): author = forms.CharField(max_length=100, required=False, label="Author(s)") title = forms.CharField(max_length=100, required=False) submitted_to = forms.ModelChoiceField( queryset=Journal.objects.active(), required=False ) identifier = forms.CharField(max_length=128, required=False) proceedings = forms.ModelChoiceField( queryset=Proceedings.objects.order_by("-submissions_close"), required=False ) def __init__(self, *args, **kwargs): self.acad_field_slug = kwargs.pop("acad_field_slug") self.specialty_slug = kwargs.pop("specialty_slug") self.reports_needed = kwargs.pop("reports_needed") super().__init__(*args, **kwargs) if self.acad_field_slug: self.fields["submitted_to"].queryset = Journal.objects.filter( college__acad_field__slug=self.acad_field_slug ) self.helper = FormHelper() self.helper.layout = Layout( Div( Div(FloatingField("author"), css_class="col-lg-6"), Div(FloatingField("title"), css_class="col-lg-6"), css_class="row mb-0", ), Div( Div(FloatingField("submitted_to"), css_class="col-lg-6"), Div(FloatingField("identifier"), css_class="col-lg-6"), css_class="row mb-0", ), Div( Div(FloatingField("proceedings"), css_class="col-lg-6"), css_class="row mb-0", css_id="row_proceedings", style="display: none", ), ) def search_results(self): """ Return all Submission objects fitting search criteria. """ submissions = Submission.objects.public_latest().unpublished() if self.acad_field_slug and self.acad_field_slug != "all": submissions = submissions.filter(acad_field__slug=self.acad_field_slug) if self.specialty_slug and self.specialty_slug != "all": submissions = submissions.filter(specialties__slug=self.specialty_slug) if self.cleaned_data.get("submitted_to"): submissions = submissions.filter( submitted_to=self.cleaned_data.get("submitted_to") ) if self.cleaned_data.get("proceedings"): submissions = submissions.filter( proceedings=self.cleaned_data.get("proceedings") ) if self.cleaned_data.get("author"): submissions = submissions.filter( author_list__icontains=self.cleaned_data.get("author") ) if self.cleaned_data.get("title"): submissions = submissions.filter( title__icontains=self.cleaned_data.get("title") ) if self.cleaned_data.get("identifier"): submissions = submissions.filter( preprint__identifier_w_vn_nr__icontains=self.cleaned_data.get( "identifier" ) ) if self.reports_needed: submissions = ( submissions.in_refereeing() .open_for_reporting() .reports_needed() .order_by("submission_date") ) return submissions class SubmissionPoolSearchForm(forms.Form): """Filter a Submission queryset using basic search fields.""" submitted_to = forms.ModelChoiceField( queryset=Journal.objects.active(), required=False ) specialties = forms.ModelMultipleChoiceField( queryset=Specialty.objects.all(), widget=autocomplete.ModelSelect2Multiple( url="/ontology/specialty-autocomplete", attrs={"data-html": True} ), label="Specialties", required=False, ) proceedings = forms.ModelChoiceField( queryset=Proceedings.objects.order_by("-submissions_close"), required=False ) author = forms.CharField(max_length=100, required=False, label="Author(s)") title = forms.CharField(max_length=512, required=False) identifier = forms.CharField(max_length=128, required=False) status = forms.ChoiceField(choices=()) editor_in_charge = forms.ModelChoiceField( queryset=Fellowship.objects.active().select_related("contributor__user"), required=False, ) versions = forms.ChoiceField( widget=forms.RadioSelect, choices=( ("latest", "Latest submitted only"), ("any", "All versions"), ), initial="latest", ) search_set = forms.ChoiceField( widget=forms.RadioSelect, choices=( ("current", "Currently under consideration"), ( "current_noawaitingresub", "Currently under consideration\n(excluding awaiting resubmission)", ), ("historical", "All accessible history"), ), initial="current", ) ordering = forms.ChoiceField( widget=forms.RadioSelect, choices=( ( "Submission date ", ( ("submission_recent", "most recent first"), ("submission_oldest", "oldest first"), ), ), ( "Activity ", ( ("activity_recent", "most recent first"), ("activity_oldest", "oldest first"), ), ), ), initial="submission_recent", ) def __init__(self, *args, **kwargs): request = kwargs.pop("request") user = request.user super().__init__(*args, **kwargs) self.fields["status"].choices = self.get_status_choices(user) if not user.contributor.is_ed_admin: # restrict journals to those of Colleges of user's Fellowships college_id_list = [ f.college.id for f in user.contributor.fellowships.active() ] self.fields["submitted_to"].queryset = Journal.objects.filter( college__in=college_id_list ) self.helper = FormHelper() self.helper.layout = Layout( Div( Div(FloatingField("submitted_to"), css_class="col-lg-6"), Div(FloatingField("specialties"), css_class="col-lg-6"), css_class="row mb-0", ), Div( Div(FloatingField("proceedings"), css_class="col-lg-6"), css_class="row mb-0", css_id="row_proceedings", style="display: none", ), Div( Div(FloatingField("author"), css_class="col-lg-6"), Div(FloatingField("title"), css_class="col-lg-6"), css_class="row mb-0", ), Div( Div(FloatingField("identifier"), css_class="col-lg-3"), Div(FloatingField("status"), css_class="col-lg-5"), Div( FloatingField("editor_in_charge"), css_class="col-lg-4", css_id="col_eic", ), css_class="row mb-0", ), Div( Div(Field("versions"), css_class="col border"), Div(Field("search_set"), css_class="col border"), Div(Field("ordering"), css_class="col border"), css_class="row mb-0", ), ) def get_status_choices(self, user): incoming = ( "Incoming", ( (Submission.INCOMING, "Incoming: awaiting EdAdmin checks"), (Submission.ADMISSION_FAILED, "Admission failed"), ( Submission.ADMISSIBLE, "Admissible; undergoing plagiarism checks", ), ( "plagiarism_internal_failed_temporary", "Failed internal plagiarism checks (temporary)", ), ( "plagiarism_internal_failed_permanent", "Failed internal plagiarism checks (permanent)", ), ( "plagiarism_iThenticate_failed_temporary", "Failed iThenticate plagiarism checks (temporary)", ), ( "plagiarism_iThenticate_failed_permanent", "Failed iThenticate plagiarism checks (permanent)", ), ), ) preassignment = ( "Preassignment", ( (Submission.PREASSIGNMENT, "In preassignment"), (Submission.PREASSIGNMENT_FAILED, "preassignment failed"), ), ) assignment = ( "Assignment", ( (Submission.SEEKING_ASSIGNMENT, "Seeking editor assignment"), ("assignment_1", "... waiting for > 1 week"), ("assignment_2", "... waiting for > 2 weeks"), ("assignment_4", "... waiting for > 4 weeks"), ( Submission.ASSIGNMENT_FAILED, "Failed to find Editor-in-charge; manuscript rejected", ), ), ) refereeing = ( "Refereeing", ( ( Submission.REFEREEING_IN_PREPARATION, "Refereeing in preparation (cycle choice needed)", ), ("in_refereeing", "In refereeing"), ("unvetted_reports", "... with unvetted Reports"), ("deadline_passed", "deadline passed, no recommendation yet"), ("refereeing_1", "Refereeing round ongoing for > 1 month"), ("refereeing_2", "Refereeing round ongoing for > 2 months"), ("refereeing_3", "Refereeing round ongoing for > 3 months"), ), ) awaiting_resubmission = ( "Awaiting resubmission", ( ( Submission.AWAITING_RESUBMISSION, "Awaiting resubmission (minor or major revision requested)", ), ), ) voting = ( "Voting", ( ("voting_prepare", "Voting in preparation"), ("voting_ongoing", "Voting ongoing"), ("voting_1", "... in voting for > 1 week"), ("voting_2", "... in voting for > 2 weeks"), ("voting_4", "... in voting for > 4 weeks"), ("nr_voted_for_gte_4", "At least 4 votes cast in favour of EiC rec"), ), ) decided = ( "Decided", ( (Submission.ACCEPTED_IN_TARGET, "Accepted in target Journal"), ( Submission.ACCEPTED_IN_ALTERNATIVE_AWAITING_PUBOFFER_ACCEPTANCE, "Accepted in other journal; awaiting puboffer acceptance", ), (Submission.ACCEPTED_IN_ALTERNATIVE, "Accepted in alternative Journal"), (Submission.REJECTED, "Rejected"), (Submission.WITHDRAWN, "Withdrawn by the Authors"), ), ) processed = (("Processed", ((Submission.PUBLISHED, "Published"),)),) if user.contributor.is_ed_admin: choices = ( ("All", (("all", "All Submissions"),)), incoming, preassignment, assignment, refereeing, awaiting_resubmission, voting, decided, ) elif user.contributor.is_active_senior_fellow: choices = ( ("All", (("all", "All Submissions"),)), preassignment, assignment, refereeing, awaiting_resubmission, voting, decided, ) else: choices = ( ("All", (("all", "All Submissions"),)), assignment, refereeing, awaiting_resubmission, voting, decided, ) return choices def search_results(self, user): """ Return all Submission objects fitting search criteria. """ latest = self.cleaned_data.get("versions") == "latest" search_set = self.cleaned_data.get("search_set") historical = search_set == "historical" submissions = Submission.objects.in_pool( user, latest=latest, historical=historical, ) if not user.contributor.is_ed_admin: submissions = submissions.stage_incoming_completed() # if not user.contributor.is_active_senior_fellow: # submissions = submissions.stage_preassignment_completed() if search_set == "current_noawaitingresub": submissions = submissions.exclude(status=Submission.AWAITING_RESUBMISSION) if self.cleaned_data.get("specialties"): submissions = submissions.filter( specialties__in=self.cleaned_data.get("specialties") ) if self.cleaned_data.get("submitted_to"): submissions = submissions.filter( submitted_to=self.cleaned_data.get("submitted_to") ) if self.cleaned_data.get("proceedings"): submissions = submissions.filter( proceedings=self.cleaned_data.get("proceedings") ) if self.cleaned_data.get("author"): submissions = submissions.filter( author_list__icontains=self.cleaned_data.get("author") ) if self.cleaned_data.get("title"): submissions = submissions.filter( title__icontains=self.cleaned_data.get("title") ) if self.cleaned_data.get("identifier"): submissions = submissions.filter( preprint__identifier_w_vn_nr__icontains=self.cleaned_data.get( "identifier" ) ) # filter by status status = self.cleaned_data.get("status") if status == "all": pass elif status == "plagiarism_internal_failed_temporary": submissions = submissions.filter( internal_plagiarism_assessment__status=PlagiarismAssessment.STATUS_FAILED_TEMPORARY, ) elif status == "plagiarism_internal_failed_permanent": submissions = submissions.filter( internal_plagiarism_assessment__status=PlagiarismAssessment.STATUS_FAILED_PERMANENT, ) elif status == "plagiarism_iThenticate_failed_temporary": submissions = submissions.filter( iThenticate_plagiarism_assessment__status=PlagiarismAssessment.STATUS_FAILED_TEMPORARY, ) elif status == "plagiarism_iThenticate_failed_permanent": submissions = submissions.filter( iThenticate_plagiarism_assessment__status=PlagiarismAssessment.STATUS_FAILED_PERMANENT, ) elif status == "assignment_1": submissions = submissions.filter( status=Submission.SEEKING_ASSIGNMENT, submission_date__lt=timezone.now() - datetime.timedelta(days=7), ) elif status == "assignment_2": submissions = submissions.filter( status=Submission.SEEKING_ASSIGNMENT, submission_date__lt=timezone.now() - datetime.timedelta(days=14), ) elif status == "assignment_4": submissions = submissions.filter( status=Submission.SEEKING_ASSIGNMENT, submission_date__lt=timezone.now() - datetime.timedelta(days=28), ) elif status == "in_refereeing": submissions = submissions.in_refereeing() elif status == "unvetted_reports": reports_to_vet = Report.objects.awaiting_vetting() id_list = [r.submission.id for r in reports_to_vet.all()] submissions = submissions.filter(id__in=id_list) elif status == "deadline_passed": submissions = ( submissions.in_refereeing() .filter( reporting_deadline__lt=timezone.now(), ) .exclude(eicrecommendations__isnull=False) ) elif status == "refereeing_1": submissions = ( submissions.filter( referee_invitations__date_invited__lt=( timezone.now() - datetime.timedelta(days=30) ) ) .exclude( referee_invitations__date_invited__lt=( timezone.now() - datetime.timedelta(days=60) ) ) .distinct() .exclude(eicrecommendations__isnull=False) ) elif status == "refereeing_2": submissions = ( submissions.filter( referee_invitations__date_invited__lt=( timezone.now() - datetime.timedelta(days=60) ) ) .exclude( referee_invitations__date_invited__lt=( timezone.now() - datetime.timedelta(days=90) ) ) .distinct() .exclude(eicrecommendations__isnull=False) ) elif status == "refereeing_3": submissions = ( submissions.filter( referee_invitations__date_invited__lt=( timezone.now() - datetime.timedelta(days=90) ) ) .distinct() .exclude(eicrecommendations__isnull=False) ) elif status == "voting_prepare": submissions = submissions.voting_in_preparation() elif status == "voting_ongoing": submissions = submissions.undergoing_voting() elif status == "voting_1": submissions = submissions.undergoing_voting(longer_than_days=7) elif status == "voting_2": submissions = submissions.undergoing_voting(longer_than_days=14) elif status == "voting_4": submissions = submissions.undergoing_voting(longer_than_days=28) elif status == "nr_voted_for_gte_4": ids_list = [ r.submission.id for r in EICRecommendation.objects.put_to_voting() .annotate( nr_voted_for=Count("voted_for"), ) .filter(nr_voted_for__gte=4) ] submissions = submissions.undergoing_voting().filter(id__in=ids_list) else: # if an actual unmodified status is used, just filter on that submissions = submissions.filter(status=status) # filter by EIC if self.cleaned_data.get("editor_in_charge"): submissions = submissions.filter( editor_in_charge=self.cleaned_data.get("editor_in_charge").contributor ) if self.cleaned_data.get("ordering") == "submission_oldest": submissions = submissions.order_by("submission_date") elif self.cleaned_data.get("ordering") == "activity_recent": submissions = submissions.order_by("-latest_activity") elif self.cleaned_data.get("ordering") == "activity_oldest": submissions = submissions.order_by("latest_activity") return submissions class ReportSearchForm(forms.Form): submission_title = forms.CharField(max_length=100, required=False) def __init__(self, *args, **kwargs): self.acad_field_slug = kwargs.pop("acad_field_slug") self.specialty_slug = kwargs.pop("specialty_slug") super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.layout = Layout( Div( Div(FloatingField("submission_title"), css_class="col-lg-6"), ), ) def search_results(self): reports = Report.objects.accepted() if self.acad_field_slug and self.acad_field_slug != "all": reports = reports.filter(submission__acad_field__slug=self.acad_field_slug) if self.specialty_slug and self.specialty_slug != "all": reports = reports.filter( submission__specialties__slug=self.specialty_slug ) if self.cleaned_data.get("submission_title"): reports = reports.filter( submission__title__icontains=self.cleaned_data.get("submission_title") ) return reports # Marked for deprecation class SubmissionOldSearchForm(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) def search_results(self): """Return all Submission objects according to search.""" return Submission.objects.public_latest().filter( title__icontains=self.cleaned_data.get("title", ""), author_list__icontains=self.cleaned_data.get("author", ""), abstract__icontains=self.cleaned_data.get("abstract", ""), ) ###################################################################### # # SubmissionForm prefill facilities. One class per integrated server. # ###################################################################### # Checks def check_resubmission_readiness(requested_by, submission): """ Check if submission can be resubmitted. """ if submission: if submission.status == Submission.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 submission.open_for_resubmission: # Check if verified author list contains current user. if requested_by.contributor not in 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 an inappropriate 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 check_identifier_is_unused(identifier): # Check if identifier has already been used for submission if Submission.objects.filter(preprint__identifier_w_vn_nr=identifier).exists(): error_message = "This preprint version has already been submitted to SciPost." raise forms.ValidationError(error_message, code="duplicate") def check_arxiv_identifier_w_vn_nr(identifier): caller = ArxivCaller(identifier) if caller.is_valid: arxiv_data = caller.data metadata = caller.metadata else: error_message = "A preprint associated to this identifier does not exist." raise forms.ValidationError(error_message) # Check if this paper has already been published (according to arXiv) published_id = None if "arxiv_doi" in arxiv_data: published_id = arxiv_data["arxiv_doi"] elif "arxiv_journal_ref" in arxiv_data: published_id = arxiv_data["arxiv_journal_ref"] if published_id: error_message = ( "This paper has been published under DOI %(published_id)s. " "It cannot be submitted again." ) raise forms.ValidationError( error_message, code="published", params={"published_id": published_id} ) return arxiv_data, metadata, identifier def check_chemrxiv_doi(doi): """ Call Crossref to get ChemRxiv preprint data. """ caller = DOICaller(doi) if caller.is_valid: data = caller.data metadata = caller.data["crossref_data"] else: error_message = "A preprint associated to this DOI does not exist." raise forms.ValidationError(error_message) # Check if the type of this resource is indeed a preprint if "subtype" in metadata: if metadata["subtype"] != "preprint": error_message = ( "This does not seem to be a preprint: the type " "returned by Crossref on behalf of " "%(preprint_server) is %(subtype). " "Please contact techsupport." ) raise forms.ValidationError( error_message, code="wrong_subtype", params={ "preprint_server": preprint_server.name, "subtype": metadata["subtype"], }, ) else: raise forms.ValidationError( "Crossref failed to return a subtype. Please contact techsupport.", code="wrong_subtype", ) # Explicitly add ChemRxiv as the preprint server: data["preprint_server"] = PreprintServer.objects.get(name="ChemRxiv") data["preprint_link"] = "https://doi.org/%s" % doi # Build the identifier by stripping the DOI prefix: identifier = doi data["identifier_w_vn_nr"] = identifier return data, metadata, identifier def check_figshare_identifier_w_vn_nr(preprint_server, figshare_identifier_w_vn_nr): """ Call Figshare to retrieve submission prefill data and perform basic checks. This method is defined outside of FigsharePrefillform in order to also be callable by SubmissionForm. """ caller = FigshareCaller(preprint_server, figshare_identifier_w_vn_nr) if caller.is_valid: figshare_data = caller.data metadata = caller.metadata else: error_message = "A preprint associated to this identifier does not exist." raise forms.ValidationError(error_message) # Check if the type of this resource is indeed a preprint if "defined_type_name" in metadata: if metadata["defined_type_name"] != "preprint": error_message = ( "This does not seem to be a preprint: the type " "returned by Figshare on behalf of " "%(preprint_server) is %(defined_type_name)s. " "Please contact techsupport." ) raise forms.ValidationError( error_message, code="wrong_defined_type_name", params={ "preprint_server": preprint_server.name, "defined_type_name": metadata["defined_type_name"], }, ) else: raise forms.ValidationError( "Figshare failed to return a defined_type_name. Please contact techsupport.", code="wrong_defined_type_name", ) # Check if this article has already been published (according to Figshare) published_id = None if "resource_doi" in metadata: published_id = metadata["resource_doi"] if published_id: error_message = ( "This paper has been published under DOI %(published_id)s. " "It cannot be submitted again." ) raise forms.ValidationError( error_message, code="published", params={"published_id": published_id} ) identifier = preprint_server.name.lower() + "_" + figshare_identifier_w_vn_nr return figshare_data, metadata, identifier # DEPRECATED def check_chemrxiv_figshare_identifier_w_vn_nr(chemrxiv_identifier_w_vn_nr): """ Call `check_figshare_identifier_w_vn_nr` but correct identifier by substituting `chemrxiv` for `figshare`. """ data, metadata, identifier = check_figshare_identifier_w_vn_nr( PreprintServer.objects.get(name="ChemRxiv"), chemrxiv_identifier_w_vn_nr ) return data, metadata, identifier.replace("figshare", "chemrxiv") def check_techrxiv_identifier_w_vn_nr(techrxiv_identifier_w_vn_nr): """ Call `check_figshare_identifier_w_vn_nr` but correct identifier by substituting `techrxiv` for `figshare`. """ data, metadata, identifier = check_figshare_identifier_w_vn_nr( PreprintServer.objects.get(name="TechRxiv"), techrxiv_identifier_w_vn_nr ) return data, metadata, identifier.replace("figshare", "techrxiv") def check_advance_identifier_w_vn_nr(advance_identifier_w_vn_nr): """ Call `check_figshare_identifier_w_vn_nr` but correct identifier by substituting `advance` for `figshare`. """ data, metadata, identifier = check_figshare_identifier_w_vn_nr( PreprintServer.objects.get(name="Advance"), advance_identifier_w_vn_nr ) return data, metadata, identifier.replace("figshare", "advance") def check_osfpreprints_identifier(preprint_server, osfpreprints_identifier): """ Call OSFPreprints to retrieve submission prefill data and perform basic checks. This method is defined outside of FigsharePrefillform in order to also be callable by SubmissionForm. """ caller = OSFPreprintsCaller(preprint_server, osfpreprints_identifier) if caller.is_valid: osfpreprints_data = caller.data metadata = caller.metadata else: error_message = "A preprint associated to this identifier does not exist." raise forms.ValidationError(error_message) # Check if the type of this resource is indeed a preprint if "type" in metadata: if metadata["type"] != "preprints": error_message = ( "This does not seem to be a preprint: the type " "returned by OSFPreprints on behalf of " "%(preprint_server) is %(type)s. " "Please contact techsupport." ) raise forms.ValidationError( error_message, code="wrong_type", params={ "preprint_server": preprint_server.name, "type": metadata["type"], }, ) else: raise forms.ValidationError( "OSFPreprints failed to return a type. Please contact techsupport.", code="wrong_type", ) # TODO: Check if this article has already been published (according to OSFPreprints) identifier = preprint_server.name.lower() + "_" + osfpreprints_identifier return osfpreprints_data, metadata, identifier def check_socarxiv_identifier(socarxiv_identifier): """ Call `check_osfpreprints_identifier_w_vn_nr` but correct identifier by substituting `socarxiv` for `osfpreprints`. """ data, metadata, identifier = check_osfpreprints_identifier( PreprintServer.objects.get(name="SocArXiv"), socarxiv_identifier ) return data, metadata, identifier.replace("osfpreprints", "socarxiv") class SubmissionPrefillForm(forms.Form): """ Base class for all SubmissionPrefillForms (one per integrated preprint server). Based on kwargs `requested_by`, `journal_doi_label` and `thread_hash`, this prepares initial data for SubmissionForm. """ def __init__(self, *args, **kwargs): self.requested_by = kwargs.pop("requested_by") self.journal = Journal.objects.get(doi_label=kwargs.pop("journal_doi_label")) self.thread_hash = kwargs.pop("thread_hash") if self.thread_hash: # Resubmission self.latest_submission = ( Submission.objects.filter(thread_hash=self.thread_hash) .order_by("-submission_date", "-preprint") .first() ) else: self.latest_submission = None super().__init__(*args, **kwargs) def is_resubmission(self): return self.latest_submission is not None def run_checks(self): """ Consistency checks on the prefill data. """ check_resubmission_readiness(self.requested_by, self.latest_submission) def get_prefill_data(self): form_data = { "acad_field": self.journal.college.acad_field, "submitted_to": self.journal, } if self.thread_hash: form_data["thread_hash"] = self.thread_hash form_data["is_resubmission_of"] = self.latest_submission.id return form_data class SciPostPrefillForm(SubmissionPrefillForm): """ Provide initial data for SubmissionForm (SciPost preprint server route). """ def is_valid(self): """ Accept an empty form as valid. Override Django BaseForm.is_valid Django BaseForm method requires is_bound == True and not self.errors. is_bound requires data is not None. We thus override is_valid by cutting the is_bound == True out. """ return not self.errors def get_prefill_data(self): """ Return initial form data originating from earlier Submission. """ form_data = super().get_prefill_data() form_data["preprint_server"] = PreprintServer.objects.get(name="SciPost") if self.is_resubmission(): form_data.update( { "title": self.latest_submission.title, "abstract": self.latest_submission.abstract, "author_list": self.latest_submission.author_list, "acad_field": self.latest_submission.acad_field, "specialties": [ s.id for s in self.latest_submission.specialties.all() ], "approaches": self.latest_submission.approaches, "referees_flagged": self.latest_submission.referees_flagged, "referees_suggested": self.latest_submission.referees_suggested, } ) return form_data class ArXivPrefillForm(SubmissionPrefillForm): """ Provide initial data for SubmissionForm (arXiv preprint server route). This adds the `arxiv_identifier_w_vn_nr` field to those from `SubmissionPrefillForm` base class. """ arxiv_identifier_w_vn_nr = forms.RegexField( label="", regex=ARXIV_IDENTIFIER_PATTERN_NEW, strip=True, error_messages={"invalid": strings.arxiv_query_invalid}, widget=forms.TextInput(), ) def __init__(self, *args, **kwargs): self.arxiv_data = {} self.metadata = {} super().__init__(*args, **kwargs) def clean_arxiv_identifier_w_vn_nr(self): """ Do basic prechecks based on the arXiv ID only. """ identifier = self.cleaned_data.get("arxiv_identifier_w_vn_nr", None) check_identifier_is_unused(identifier) self.arxiv_data, self.metadata, identifier = check_arxiv_identifier_w_vn_nr( identifier ) return identifier def get_prefill_data(self): """ Return dictionary to prefill `SubmissionForm`. """ form_data = super().get_prefill_data() form_data.update(self.arxiv_data) form_data["identifier_w_vn_nr"] = self.cleaned_data["arxiv_identifier_w_vn_nr"] if self.is_resubmission(): form_data.update( { "approaches": self.latest_submission.approaches, "referees_flagged": self.latest_submission.referees_flagged, "referees_suggested": self.latest_submission.referees_suggested, "acad_field": self.latest_submission.acad_field, "specialties": [ s.id for s in self.latest_submission.specialties.all() ], } ) return form_data class ChemRxivPrefillForm(SubmissionPrefillForm): """ Provide initial data for SubmissionForm from ChemRxiv (metadata actually collected from Crossref API, not ChemRxiv). This form is used by the ChemRxiv route (post-2021-07 style). """ chemrxiv_doi = forms.RegexField( label="", regex=CHEMRXIV_DOI_PATTERN, strip=True, error_messages={"invalid": "Invalid ChemRxiv DOI"}, widget=forms.TextInput(), ) def __init__(self, *args, **kwargs): self.crossref_data = {} self.metadata = {} super().__init__(*args, **kwargs) def clean_chemrxiv_doi(self): # To get the identifier, strip the DOI prefix identifier = self.cleaned_data.get("chemrxiv_doi", None).partition("/")[2] check_identifier_is_unused(identifier) self.crossref_data, self.metadata, identifier = check_chemrxiv_doi( self.cleaned_data["chemrxiv_doi"] ) return identifier def get_prefill_data(self): """ Return dictionary to prefill `SubmissionForm`. """ form_data = super().get_prefill_data() form_data.update(self.crossref_data) if self.is_resubmission(): form_data.update( { "approaches": self.latest_submission.approaches, "referees_flagged": self.latest_submission.referees_flagged, "referees_suggested": self.latest_submission.referees_suggested, "acad_field": self.latest_submission.acad_field, "specialties": [ s.id for s in self.latest_submission.specialties.all() ], } ) return form_data class FigsharePrefillForm(SubmissionPrefillForm): """ Provide initial data for SubmissionForm from Figshare. This form is used by the ChemRxiv (pre-2021-07), TechRxiv and Advance routes. """ figshare_preprint_server = forms.ModelChoiceField( queryset=PreprintServer.objects.filter(served_by__name="Figshare"), widget=forms.HiddenInput(), ) figshare_identifier_w_vn_nr = forms.RegexField( label="", regex=FIGSHARE_IDENTIFIER_PATTERN, strip=True, error_messages={"invalid": "Invalid Figshare identifier"}, widget=forms.TextInput(), ) def __init__(self, *args, **kwargs): self.figshare_data = {} self.metadata = {} self.identifier = None super().__init__(*args, **kwargs) def clean_figshare_identifier_w_vn_nr(self): """ Do basic prechecks based on the Figshare identifier. """ ( self.figshare_data, self.metadata, self.identifier, ) = check_figshare_identifier_w_vn_nr( self.cleaned_data["figshare_preprint_server"], self.cleaned_data["figshare_identifier_w_vn_nr"], ) check_identifier_is_unused(self.identifier) return self.cleaned_data["figshare_identifier_w_vn_nr"] def get_prefill_data(self): """ Return dictionary to prefill `SubmissionForm`. """ form_data = super().get_prefill_data() form_data.update(self.figshare_data) if self.is_resubmission(): form_data.update( { "approaches": self.latest_submission.approaches, "referees_flagged": self.latest_submission.referees_flagged, "referees_suggested": self.latest_submission.referees_suggested, "acad_field": self.latest_submission.acad_field, "specialties": [ s.id for s in self.latest_submission.specialties.all() ], } ) return form_data class OSFPreprintsPrefillForm(SubmissionPrefillForm): """ Provide initial data for SubmissionForm from OSFPreprints. This form is used by the SocArXiv (and others) routes. """ osfpreprints_preprint_server = forms.ModelChoiceField( queryset=PreprintServer.objects.filter(served_by__name="OSFPreprints"), widget=forms.HiddenInput(), ) osfpreprints_identifier = forms.RegexField( label="", regex=OSFPREPRINTS_IDENTIFIER_PATTERN, strip=True, error_messages={"invalid": "Invalid OSFPreprints identifier"}, widget=forms.TextInput(), ) def __init__(self, *args, **kwargs): self.osfpreprints_data = {} self.metadata = {} self.identifier = None super().__init__(*args, **kwargs) def clean_osfpreprints_identifier(self): """ Do basic prechecks based on the OSFPreprints identifier. """ ( self.osfpreprints_data, self.metadata, self.identifier, ) = check_osfpreprints_identifier( self.cleaned_data["osfpreprints_preprint_server"], self.cleaned_data["osfpreprints_identifier"], ) check_identifier_is_unused(self.identifier) return self.cleaned_data["osfpreprints_identifier"] def get_prefill_data(self): """ Return dictionary to prefill `SubmissionForm`. """ form_data = super().get_prefill_data() form_data.update(self.osfpreprints_data) if self.is_resubmission(): form_data.update( { "approaches": self.latest_submission.approaches, "referees_flagged": self.latest_submission.referees_flagged, "referees_suggested": self.latest_submission.referees_suggested, "acad_field": self.latest_submission.acad_field, "specialties": [ s.id for s in self.latest_submission.specialties.all() ], } ) return form_data ################### # # Submission form # ################### class SubmissionForm(forms.ModelForm): """ Form to submit a new (re)Submission. """ specialties = forms.ModelMultipleChoiceField( queryset=Specialty.objects.all(), widget=autocomplete.ModelSelect2Multiple( url="/ontology/specialty-autocomplete", attrs={"data-html": True} ), label="Specialties", help_text="Type to search, click to include", ) topics = forms.ModelMultipleChoiceField( queryset=Topic.objects.all(), widget=autocomplete.ModelSelect2Multiple( url="/ontology/topic-autocomplete", attrs={"data-html": True}, forward=[ "specialties", ], ), help_text="Type to search, click to include", required=False, ) followup_of = forms.ModelMultipleChoiceField( queryset=Publication.objects.all(), widget=autocomplete.ModelSelect2Multiple( url="/journals/own-publication-autocomplete", attrs={ "data-html": True, "data-placeholder": "Optional", }, ), required=False, help_text="<strong>Does this Submission follow up on some of your earlier publications?<br>(for example: this Submission is a new codebase release for a previous Codebases publication)<br>If so, select them here.</strong><br><strong>This is NOT FOR SPECIFYING A RESUBMISSION: to resubmit a manuscript, choose the resubmission route after clicking the Submit button in the navbar.", ) preprint_server = forms.ModelChoiceField( queryset=PreprintServer.objects.all(), widget=forms.HiddenInput() ) preprint_link = forms.URLField(widget=forms.HiddenInput()) 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)" ), required=False, ) class Meta: model = Submission fields = [ "is_resubmission_of", "thread_hash", "submitted_to", "proceedings", "acad_field", "specialties", "topics", "approaches", "title", "author_list", "abstract", "followup_of", "code_repository_url", "data_repository_url", "author_comments", "list_of_changes", "remarks_for_editors", "referees_suggested", "referees_flagged", ] widgets = { "submitted_to": forms.HiddenInput(), "acad_field": forms.HiddenInput(), "is_resubmission_of": forms.HiddenInput(), "thread_hash": forms.HiddenInput(), "code_repository_url": forms.TextInput( attrs={"placeholder": "If applicable; please give the full URL"} ), "data_repository_url": forms.TextInput( attrs={"placeholder": "If applicable; please give the full URL"} ), "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.submitted_to_journal = kwargs.pop("submitted_to_journal") data = args[0] if len(args) > 1 else kwargs.get("data", {}) self.preprint_server = kwargs["initial"].get( "preprint_server", None ) or PreprintServer.objects.get(id=data.get("preprint_server")) self.thread_hash = kwargs["initial"].get("thread_hash", None) or data.get( "thread_hash" ) self.is_resubmission_of = kwargs["initial"].get( "is_resubmission_of", None ) or data.get("is_resubmission_of") self.preprint_data = {} self.metadata = {} # container for possible external server-provided metadata super().__init__(*args, **kwargs) if self.preprint_server.name == "SciPost": # SciPost identifier will be auto-generated del self.fields["identifier_w_vn_nr"] # Preprint will be linked directly from preprint file object del self.fields["preprint_link"] else: # No need for a file upload if user is not using the SciPost preprint server. del self.fields["preprint_file"] if not self.is_resubmission(): del self.fields["is_resubmission_of"] del self.fields["author_comments"] del self.fields["list_of_changes"] # Restrict choice of specialties to those of relevant AcademicField if kwargs["initial"].get("acad_field", None): self.fields["specialties"].widget.url = ( self.fields["specialties"].widget.url + "?acad_field_id=" + str(kwargs["initial"].get("acad_field").id) ) # Proceedings submission fields if "Proc" not in self.submitted_to_journal.doi_label: del self.fields["proceedings"] else: qs = self.fields["proceedings"].queryset.open_for_submission() self.fields["proceedings"].queryset = qs self.fields["proceedings"].empty_label = None if not qs.exists(): del self.fields["proceedings"] def is_resubmission(self): return self.is_resubmission_of is not None def clean(self, *args, **kwargs): """ Do all general checks for Submission. """ cleaned_data = super().clean(*args, **kwargs) # SciPost preprints are auto-generated here. if "identifier_w_vn_nr" not in cleaned_data: cleaned_data["identifier_w_vn_nr"] = get_new_scipost_identifier( thread_hash=self.thread_hash ) if self.is_resubmission(): check_resubmission_readiness( self.requested_by, cleaned_data["is_resubmission_of"] ) self.clear_submission_object_types() if "Proc" not in cleaned_data["submitted_to"].doi_label: try: del self.cleaned_data["proceedings"] except KeyError: # No proceedings returned to data pass return cleaned_data def clear_submission_object_types(self): """ Check that the submitted material fits one of the Journal's options. """ submitted_types = [] if self.cleaned_data.get("preprint_file", None) or self.cleaned_data.get( "preprint_link", None ): submitted_types.append(PUBLISHABLE_OBJECT_TYPE_ARTICLE) if self.cleaned_data.get("code_repository_url", None): submitted_types.append(PUBLISHABLE_OBJECT_TYPE_CODEBASE) if self.cleaned_data.get("data_repository_url", None): submitted_types.append(PUBLISHABLE_OBJECT_TYPE_DATASET) submitted_types.sort() # not needed here, but for future safety submitted_types_code = " + ".join(submitted_types) options = self.cleaned_data["submitted_to"].submission_object_types["options"] if submitted_types_code not in options: self.add_error( None, ( f"You are trying to submit document types: {submitted_types_code}, " "but this Journal requires one of the following options: " f"{', '.join(options)}" ), ) 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 def clean_code_repository_url(self): """ Prevent having well-known servers in list. """ code_repository_url = self.cleaned_data["code_repository_url"] if "arxiv.org" in str(code_repository_url).lower(): error_message = ( "ArXiv.org is not a code repository; " "did you perhaps use the wrong form field?" ) self.add_error("code_repository_url", error_message) return code_repository_url def clean_data_repository_url(self): """ Prevent having well-known servers in list. """ data_repository_url = self.cleaned_data["data_repository_url"] if "arxiv.org" in str(data_repository_url).lower(): error_message = ( "ArXiv.org is not a data repository; " "did you perhaps use the wrong form field?" ) self.add_error("data_repository_url", error_message) return data_repository_url def clean_identifier_w_vn_nr(self): identifier = self.cleaned_data.get("identifier_w_vn_nr", None) check_identifier_is_unused(identifier) if self.preprint_server.name == "arXiv": ( self.preprint_data, self.metadata, identifier, ) = check_arxiv_identifier_w_vn_nr(identifier) elif self.preprint_server.name == "ChemRxiv": self.preprint_data, self.metadata, identifier = check_chemrxiv_doi( identifier ) elif self.preprint_server.name == "TechRxiv": ( self.preprint_data, self.metadata, identifier, ) = check_techrxiv_identifier_w_vn_nr(identifier.replace("techrxiv_", "")) elif self.preprint_server.name == "Advance": ( self.preprint_data, self.metadata, identifier, ) = check_advance_identifier_w_vn_nr(identifier.replace("advance_", "")) elif self.preprint_server.name == "SocArXiv": self.preprint_data, self.metadata, identifier = check_socarxiv_identifier( identifier.replace("socarxiv_", "") ) else: error_message = ( "Check method not implemented for preprint server: %s. " "Please contact techsupport." ) % self.preprint_server self.add_error("identifier_w_vn_nr", error_message) return identifier @transaction.atomic def save(self): """ Create the new Submission and Preprint instances. """ submission = super().save(commit=False) submission.submitted_by = self.requested_by.contributor submission.reporting_deadline = ( # give 8 days for Admission, Preassignment and Assignment stages timezone.now() + datetime.timedelta(days=8) + self.cleaned_data["submitted_to"].refereeing_period ) # Save identifiers url = "" if self.cleaned_data.get("preprint_link", None): url = self.cleaned_data["preprint_link"] preprint, __ = Preprint.objects.get_or_create( identifier_w_vn_nr=self.cleaned_data["identifier_w_vn_nr"], url=url, _file=self.cleaned_data.get("preprint_file", None), ) # Save metadata directly from preprint server call without possible user interception submission.metadata = self.metadata submission.preprint = preprint submission.save() # Try to match the submitting author's last name to a position from the author list. try: submitting_author_order = list( map( lambda x: self.requested_by.contributor.profile.last_name in x.strip(), submission.author_list.split(","), ) ).index(True) except ValueError: # Otherwise, assume the submitting author is the first author. submitting_author_order = 1 # Add the submitter's AuthorProfile: author_profile = SubmissionAuthorProfile( submission=submission, profile=self.requested_by.contributor.profile, order=submitting_author_order, ) author_profile.save() submission.author_profiles.add(author_profile) # Explicitly handle specialties (otherwise they are not saved) submission.specialties.set(self.cleaned_data["specialties"]) if self.is_resubmission(): self.process_resubmission(submission) # Gather first known author and Fellows. submission.authors.add(self.requested_by.contributor) self.set_fellowship(submission) # Return latest version of the Submission. It could be outdated by now. submission.refresh_from_db() return submission def process_resubmission(self, submission): """ Update all fields for new and old Submission and EditorialAssignments. -- submission: the new version of the Submission series. """ if not submission.is_resubmission_of: raise Submission.DoesNotExist previous_submission = submission.is_resubmission_of # Close last submission Submission.objects.filter(id=previous_submission.id).update( open_for_reporting=False, status=Submission.RESUBMITTED ) # Copy Topics submission.topics.add(*previous_submission.topics.all()) # Open for comments (reports: opened upon cycle choice) and copy EIC info Submission.objects.filter(id=submission.id).update( open_for_commenting=True, open_for_reporting=False, visible_public=previous_submission.visible_public, visible_pool=True, refereeing_cycle=CYCLE_UNDETERMINED, editor_in_charge=previous_submission.editor_in_charge, status=Submission.REFEREEING_IN_PREPARATION, ) # Add author(s) (claim) fields submission.authors.add(*previous_submission.authors.all()) submission.authors_claims.add(*previous_submission.authors_claims.all()) submission.authors_false_claims.add( *previous_submission.authors_false_claims.all() ) # Create new EditorialAssigment for the current Editor-in-Charge EditorialAssignment.objects.create( submission=submission, to=previous_submission.editor_in_charge, status=EditorialAssignment.STATUS_ACCEPTED, ) def set_fellowship(self, submission): """ Set the default set of (guest) Fellows for this Submission. """ qs = Fellowship.objects.active() if submission.proceedings: # Add only Proceedings-related Fellowships fellows = qs.filter( proceedings=submission.proceedings ).return_active_for_submission(submission) submission.fellows.set(fellows) else: fellows = ( qs.regular_or_senior() .filter( college=submission.submitted_to.college, contributor__profile__specialties__in=submission.specialties.all(), ) .return_active_for_submission(submission) ) submission.fellows.set(fellows) class SubmissionReportsForm(forms.ModelForm): """Update refereeing pdf for Submission.""" class Meta: model = Submission fields = ["pdf_refereeing_pack"] class PreassignEditorsForm(forms.ModelForm): """Preassign editors during Submission preassignment.""" 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 == Submission.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): """Pre-assign editors during Submission preassignment.""" 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 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, ) class SubmissionReassignmentForm(forms.ModelForm): """Process reassignment of EIC for Submission.""" new_editor = forms.ModelChoiceField( queryset=Contributor.objects.none(), required=True ) email_old_eic = forms.BooleanField( required=False, initial=True, help_text="Whether the previous EiC should be informed", ) 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=EditorialAssignment.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=EditorialAssignment.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 and self.cleaned_data["email_old_eic"]: 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() class SubmissionTargetJournalForm(forms.ModelForm): """Change the target journal for the Submission.""" class Meta: model = Submission fields = [ "submitted_to", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["submitted_to"].queryset = Journal.objects.active() self.helper = FormHelper() self.helper.layout = Layout( FloatingField("submitted_to"), ButtonHolder(Submit("submit", "Update", css_class="btn btn-danger")), ) class SubmissionTargetProceedingsForm(forms.ModelForm): """Change the target Proceedings for the Submission.""" class Meta: model = Submission fields = ["proceedings"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["proceedings"].queryset = Proceedings.objects.order_by( "-submissions_close" ) self.fields["proceedings"].help_text = None self.helper = FormHelper() self.helper.layout = Layout( FloatingField("proceedings"), ButtonHolder(Submit("submit", "Update", css_class="btn btn-danger")), ) class SubmissionPreprintFileForm(forms.ModelForm): """Change the submitted pdf for the Submission.""" class Meta: model = Preprint fields = ["_file"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper() self.helper.layout = Layout( Field("_file"), ButtonHolder(Submit("submit", "Update", css_class="btn btn-danger")), ) class SubmissionPreassignmentForm(forms.ModelForm): """Processing decision for preassignment of Submission.""" PASS, FAIL = "pass", "fail" CHOICES = ( (PASS, "Pass preassignment. Proceed to the Pool."), (FAIL, "Fail preassignment."), ) 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 != Submission.PREASSIGNMENT: self.add_error(None, "This Submission is currently not in preassignment.") 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=Submission.SEEKING_ASSIGNMENT, visible_pool=True, visible_public=False, ) self.instance.add_general_event("Submission passed preassignment.") elif self.cleaned_data["decision"] == self.FAIL: EditorialAssignment.objects.filter( submission=self.instance ).invited().update(status=EditorialAssignment.STATUS_DEPRECATED) Submission.objects.filter(id=self.instance.id).update( status=Submission.PREASSIGNMENT_FAILED, visible_pool=False, visible_public=False, ) self.instance.add_general_event("Submission failed preassignment.") mail_sender = DirectMailUtil( "preassignment_failed", instance=self.instance, message_for_authors=self.cleaned_data["message_for_authors"], header_template="submissions/admin/preassignment_failed.html", ) mail_sender.send_mail() if self.cleaned_data["message_for_authors"]: Remark.objects.create( submission=self.instance, contributor=self.current_user.contributor, remark=self.cleaned_data["message_for_authors"], ) 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"], ) 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) 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.submission.id).update( visible_public=False, visible_pool=False, open_for_commenting=False, open_for_reporting=False, status=Submission.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=EditorialAssignment.STATUS_DEPRECATED) EditorialAssignment.objects.filter( submission=self.submission ).accepted().update(status=EditorialAssignment.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(): decision = EditorialDecision.objects.filter( submission=self.submission ).latest_version() decision.status = EditorialDecision.PUBOFFER_REFUSED_BY_AUTHORS decision.save() # 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 # ###################### 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=EditorialAssignment.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=Submission.IN_REFEREEING, 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: # Direct editorial recommendation visible_public = False if self.instance.submission.is_resubmission_of: visible_public = ( self.instance.submission.is_resubmission_of.visible_public ) Submission.objects.filter(id=self.submission.id).update( refereeing_cycle=CYCLE_DIRECT_REC, status=Submission.REFEREEING_CLOSED, editor_in_charge=self.request.user.contributor, reporting_deadline=timezone.now(), open_for_reporting=False, open_for_commenting=True, visible_public=visible_public, 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.status = EditorialAssignment.STATUS_ACCEPTED # Update all other 'open' invitations EditorialAssignment.objects.filter( submission=self.submission ).need_response().exclude(id=assignment.id).update( status=EditorialAssignment.STATUS_DEPRECATED, ) else: assignment.status = EditorialAssignment.STATUS_DECLINED assignment.refusal_reason = self.cleaned_data["refusal_reason"] assignment.save() # Save again to register acceptance return assignment class RefereeSearchForm(forms.Form): last_name = forms.CharField( widget=forms.TextInput( {"placeholder": "Search for a referee in the SciPost Profiles database"} ) ) def search(self): query = Q_with_alternative_spellings( last_name__icontains=self.cleaned_data["last_name"] ) return ( Profile.objects.filter(query) .exclude(contributor__user__is_superuser=True) .exclude(contributor__user__is_staff=True) ) class ConsiderRefereeInvitationForm(forms.Form): accept = forms.ChoiceField( widget=forms.RadioSelect, choices=((True, "Accept"), (False, "Decline")), label="Are you willing to referee this Submission?", ) refusal_reason = forms.ChoiceField( choices=EditorialAssignment.REFUSAL_REASONS, required=False ) class SetRefereeingDeadlineForm(forms.Form): deadline = forms.DateField( 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") >= timezone.now().date()): self.add_error("deadline", "Please choose a future 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( 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) # 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(profile__specialties__in=self.instance.submission.specialties.all()) | Q(pk__in=prev_elig_id) ) .order_by("user__last_name") .distinct() ) self.fields["eligible_fellows"].queryset = eligible 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 def get_eligible_fellows(self): return self.fields["eligible_fellows"].queryset ############ # Reports: ############ class ReportPDFForm(forms.ModelForm): class Meta: model = Report fields = ["pdf_report"] class ReportForm(forms.ModelForm): """Write Report form.""" report_type = REPORT_NORMAL 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. Are this Journal's acceptance criteria met? Would you recommend publication in another Journal instead?", "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 (2MB limit; for a figure or similar - please avoid annotated pdfs)" # 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 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 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, } ) 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 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 # ################### class EditorialCommunicationForm(forms.ModelForm): class Meta: model = EditorialCommunication fields = ("text",) widgets = { "text": forms.Textarea( attrs={"rows": 5, "placeholder": "Write your message in this box."} ), } ###################### # EIC Recommendation # ###################### 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 ) 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 remarks for the authors. If you recommend to accept or reject, will" " only be seen after the college vote concludes." ), "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 reject the manuscript, the Editorial College" " will vote. Summarize the reasons for your recommendation. Focus especially" " on the aspects that do not directly follow from the referee reports." ), } ), } 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) for_journal_qs = Journal.objects.active().filter( # The journals which can be recommended for are those falling under # the responsibility of the College of the journal submitted to college=self.submission.submitted_to.college ) if self.submission.submitted_to.name.partition(" ")[0] == "SciPost": # Submitted to a SciPost journal, so Selections is accessible for_journal_qs = for_journal_qs | Journal.objects.filter( name="SciPost Selections" ) self.fields["for_journal"].queryset = for_journal_qs if self.submission.submitted_to.name.partition(" ")[0] == "SciPost": # Submitted to a SciPost journal, so Core and Selections are accessible 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() 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." ) if ( cleaned_data["recommendation"] in (EIC_REC_PUBLISH, EIC_REC_REJECT) and len(cleaned_data["remarks_for_editorial_college"]) < 10 ): raise forms.ValidationError( "You must substantiate your recommendation to accept or reject the manuscript." ) def save(self): # If the cycle hadn't been chosen, set it to the DirectCycle if not self.submission.refereeing_cycle: self.submission.refereeing_cycle = CYCLE_DIRECT_REC self.submission.save() 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(), status=Submission.AWAITING_RESUBMISSION, ) if self.assignment: # The EIC has fulfilled this editorial assignment. self.assignment.status = EditorialAssignment.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(), ) ) elif recommendation.recommendation in [ EIC_REC_PUBLISH, EIC_REC_REJECT, ]: # 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() # set correct status for Submission Submission.objects.filter(id=self.submission.id).update( open_for_reporting=False, open_for_commenting=False, reporting_deadline=timezone.now(), status=Submission.VOTING_IN_PREPARATION, ) # Add SubmissionEvent for EIC only self.submission.add_event_for_eic( event_text.format( str(recommendation.for_journal), recommendation.get_recommendation_display(), ) ) else: raise exceptions.InvalidRecommendationError(recommendation.recommendation) 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() # The EIC should vote in favour of their own recommendation # This should be done after the recommendation is saved, so that the # id is determined for use in the ManyToMany relation. # Tiering has already been created above, and no special objects are required # in the event of submission rejection. recommendation.voted_for.add(self.submission.editor_in_charge) recommendation.save() return recommendation def revision_requested(self): return self.instance.recommendation in [ EIC_REC_MINOR_REVISION, EIC_REC_MAJOR_REVISION, ] def has_assignment(self): return self.assignment is not None 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 def load_earlier_recommendations(self): """Load and save EICRecommendations related to Submission of the instance.""" self.earlier_recommendations = self.submission.eicrecommendations.all() ############### # Vote form # ############### 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, ) 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)." ) class EditorialDecisionForm(forms.ModelForm): """For EdAdmin to fix the outcome on a Submission, after voting is completed.""" 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 "submission" in self.initial: self.fields["submission"].queryset = Submission.objects.filter( pk=self.initial["submission"], ) self.fields["submission"].disabled = True 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]"} ) 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." ) 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() .latest_version() .version + 1 ) decision.save() return decision 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) def is_confirmed(self): return self.cleaned_data.get("confirm") in (True, "True") def save(self): if self.is_confirmed(): Submission.objects.filter(id=self.submission.id).update( status=Submission.REFEREEING_IN_PREPARATION, refereeing_cycle=CYCLE_UNDETERMINED, acceptance_date=None, latest_activity=timezone.now(), ) self.submission.editorial_assignments.filter( to=self.submission.editor_in_charge, status=EditorialAssignment.STATUS_COMPLETED, ).update(status=EditorialAssignment.STATUS_ACCEPTED) self.submission.eicrecommendations.active().update(status=DEPRECATED) self.submission.editorialdecision_set.update( status=EditorialDecision.DEPRECATED ) # Delete any production stream if hasattr(self.submission, "production_stream"): self.submission.production_stream.delete() self.submission.refresh_from_db() return self.submission class SubmissionCycleChoiceForm(forms.ModelForm): """ For the EIC to take a decision on the Submission's cycle. Used for resubmissions only. """ referees_reinvite = forms.ModelMultipleChoiceField( queryset=RefereeInvitation.objects.none(), widget=forms.CheckboxSelectMultiple({"checked": "checked"}), required=False, label="Reinvite referees", ) 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() def save(self): """ If the cycle is for a normal or short refereeing round, open the sub for reporting. """ if self.cleaned_data["refereeing_cycle"] in [CYCLE_DEFAULT, CYCLE_SHORT]: self.instance.open_for_reporting = True return super().save() class iThenticateReportForm(forms.ModelForm): class Meta: model = iThenticateReport fields = [] 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() 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) cleaned_data["document"] = self.submission.preprint.get_document() except exceptions.PreprintDocumentNotFoundError: self.add_error( None, "Preprint document not found. Please upload the pdf manually." ) self.fields[ "file" ] = forms.FileField() # Add this field now it's needed 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"] 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 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( iThenticate_plagiarism_report=report ) return report 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() 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 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 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