From 48b3fb750b6d248cdff86821781a9f1216149a65 Mon Sep 17 00:00:00 2001 From: "J.-S. Caux" <J.S.Caux@uva.nl> Date: Sun, 19 Jul 2020 17:31:28 +0200 Subject: [PATCH] Full submission workflow reimplemented; ready to start testing --- preprints/factories.py | 1 - preprints/helpers.py | 103 +++++-- ...ve_preprint_scipost_preprint_identifier.py | 17 ++ preprints/models.py | 11 +- .../scipost/personal_page/submissions.html | 2 +- submissions/forms.py | 260 ++++++++++++------ .../submit_choose_preprint_server.html | 1 - 7 files changed, 274 insertions(+), 121 deletions(-) create mode 100644 preprints/migrations/0011_remove_preprint_scipost_preprint_identifier.py diff --git a/preprints/factories.py b/preprints/factories.py index 70bd63418..da1fac394 100644 --- a/preprints/factories.py +++ b/preprints/factories.py @@ -21,7 +21,6 @@ class PreprintFactory(factory.django.DjangoModelFactory): o.identifier_wo_vn_nr, o.vn_nr)) url = factory.lazy_attribute(lambda o: ( 'https://arxiv.org/abs/%s' % o.identifier_wo_vn_nr)) - scipost_preprint_identifier = factory.Sequence(lambda n: Preprint.objects.count() + 1) class Meta: model = Preprint diff --git a/preprints/helpers.py b/preprints/helpers.py index 8c18ae956..78752a6f7 100644 --- a/preprints/helpers.py +++ b/preprints/helpers.py @@ -10,35 +10,84 @@ from submissions.models import Submission from .models import Preprint -def generate_new_scipost_identifier(old_preprint=None): +def get_new_scipost_identifier(thread_hash=None): """ - Return an identifier for a new SciPost preprint series without version number. + Return an identifier for a new SciPost preprint (consistent with thread history). - TODO: This method will explode as soon as it will be used similtaneously by two or more people. + A SciPost identifier is of the form [YYYY][MM]_[#####]v[vn_nr]. + + For an existing thread, different cases must be treated: + + * All preprints in thread are SciPost preprints: the vn_nr is incremented. + + * Previous preprints are all on an external preprint server: a brand new SciPost + identifier is generated; the vn_nr is put to [nr of previous subs in thread] + 1. + + * Previous preprints mix SciPost and external identifiers: the SciPost identifier is + reused, putting the vn_nr to [nr of previous subs in thread] + 1. """ now = timezone.now() - if isinstance(old_preprint, Submission): - old_preprint = old_preprint.preprint - - if old_preprint: - # Generate new version number of existing series. - preprint_series = Preprint.objects.filter( - scipost_preprint_identifier=old_preprint.scipost_preprint_identifier).values_list( - 'vn_nr', flat=True) - identifier = '{}v{}'.format(old_preprint.identifier_wo_vn_nr, max(preprint_series) + 1) - return identifier, old_preprint.scipost_preprint_identifier - else: - # New series of Preprints. - existing_identifier = Preprint.objects.filter( - created__year=now.year, created__month=now.month).aggregate( - identifier=Max('scipost_preprint_identifier'))['identifier'] - if not existing_identifier: - existing_identifier = '1' - else: - existing_identifier = str(existing_identifier + 1) - - identifier = 'scipost_{year}{month}_{identifier}v1'.format( - year=now.year, month=str(now.month).rjust(2, '0'), - identifier=existing_identifier.rjust(5, '0')) - return identifier, int(existing_identifier) + submissions_in_thread = Submission.objects.filter(thread_hash=thread_hash) + + scipost_submissions_in_thread = submissions_in_thread.filter( + preprint__identifier_w_vn_nr__startswith='scipost') + + # At least one previous submission on SciPost's preprint server + if len(scipost_submissions_in_thread) > 0: + identifier = '{}v{}'.format( + scipost_submissions_in_thread.first().identifier_wo_vn_nr, + str(len(scipost_submissions_in_thread) + 1)) + return identifier + + # No previous Submission, or no previous SciPost preprint in thread; new identifier + current_identifier_prefix = 'scipost_%s%s' % (now.year, str(now.month).rjust(2,'0')) + try: + next_identifier_nr = int(Preprint.objects.filter( + identifier_w_vn_nr__startswith=current_identifier_prefix + ).first().identifier_w_vn_nr.rpartition('v')[0].rpartition('_')[2]) + 1 + except AttributeError: + next_identifier_nr = 1 + + identifier = 'scipost_{year}{month}_{identifier}v{vn_nr}'.format( + year=now.year, month=str(now.month).rjust(2, '0'), + identifier=str(next_identifier_nr).rjust(5, '0'), + vn_nr=str(len(submissions_in_thread) + 1) + ) + return identifier + + +# def generate_new_scipost_identifier_BUGGED(old_preprint=None): +# """ +# Return an identifier for a new SciPost preprint series without version number. + +# TODO: This method will explode as soon as it will be used similtaneously by two or more people. +# """ +# now = timezone.now() + +# if isinstance(old_preprint, Submission): +# old_preprint = old_preprint.preprint + +# if old_preprint: +# # Generate new version number of existing series. +# # BUGGED! This fails to make scipost_preprint_identifier globally unique +# # BUGGED! Instead, scipost_preprint_identifier can be repeated each month. +# preprint_series = Preprint.objects.filter( +# scipost_preprint_identifier=old_preprint.scipost_preprint_identifier).values_list( +# 'vn_nr', flat=True) +# identifier = '{}v{}'.format(old_preprint.identifier_wo_vn_nr, max(preprint_series) + 1) +# return identifier, old_preprint.scipost_preprint_identifier +# else: +# # New series of Preprints. +# existing_identifier = Preprint.objects.filter( +# created__year=now.year, created__month=now.month).aggregate( +# identifier=Max('scipost_preprint_identifier'))['identifier'] +# if not existing_identifier: +# existing_identifier = '1' +# else: +# existing_identifier = str(existing_identifier + 1) + +# identifier = 'scipost_{year}{month}_{identifier}v1'.format( +# year=now.year, month=str(now.month).rjust(2, '0'), +# identifier=existing_identifier.rjust(5, '0')) +# return identifier, int(existing_identifier) diff --git a/preprints/migrations/0011_remove_preprint_scipost_preprint_identifier.py b/preprints/migrations/0011_remove_preprint_scipost_preprint_identifier.py new file mode 100644 index 000000000..306529a65 --- /dev/null +++ b/preprints/migrations/0011_remove_preprint_scipost_preprint_identifier.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.11 on 2020-07-19 15:30 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preprints', '0010_merge_20181207_1008'), + ] + + operations = [ + migrations.RemoveField( + model_name='preprint', + name='scipost_preprint_identifier', + ), + ] diff --git a/preprints/models.py b/preprints/models.py index 511278ef5..ead67904e 100644 --- a/preprints/models.py +++ b/preprints/models.py @@ -8,10 +8,10 @@ from django.http import Http404 class Preprint(models.Model): - """A link with arXiv or standalone/SciPost-hosted preprint. + """ + A preprint object, either at SciPost or with link to external preprint server. - If the instance is a SciPost preprint, the `_file` and `scipost_preprint_identifier` fields - should be filled. Otherwise, these fields should be left blank. + If the instance is a SciPost preprint, the `_file` field should be filled. """ # (arXiv) identifiers with/without version number @@ -20,10 +20,7 @@ class Preprint(models.Model): vn_nr = models.PositiveSmallIntegerField(verbose_name='Version number', default=1) url = models.URLField(blank=True) - # SciPost-preprints only - scipost_preprint_identifier = models.PositiveIntegerField( - verbose_name='SciPost preprint ID', - null=True, blank=True, help_text='Unique identifier for SciPost standalone preprints') + # SciPost preprints only _file = models.FileField( verbose_name='Preprint file', help_text='Preprint file for SciPost standalone preprints', upload_to='UPLOADS/PREPRINTS/%Y/%m/', max_length=200, blank=True) diff --git a/scipost/templates/partials/scipost/personal_page/submissions.html b/scipost/templates/partials/scipost/personal_page/submissions.html index 1248fa733..10dad4e86 100644 --- a/scipost/templates/partials/scipost/personal_page/submissions.html +++ b/scipost/templates/partials/scipost/personal_page/submissions.html @@ -31,7 +31,7 @@ <p class="card-text mt-1"> <ul> {% if sub.open_for_resubmission %} - <li><a href="{% url 'submissions:submit_choose_journal' discipline=sub.discipline thread_hash=sub.thread_hash %}"><i class="fa fa-arrow-right"></i> resubmit</a></li> + <li><a href="{% url 'submissions:submit_choose_journal' discipline=sub.discipline %}?thread_hash={{ sub.thread_hash }}"><i class="fa fa-arrow-right"></i> resubmit</a></li> {% endif %} {% if sub.under_consideration %} {% if sub.editor_in_charge %} diff --git a/submissions/forms.py b/submissions/forms.py index ff7d69de4..5ec6f0865 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -38,7 +38,7 @@ from colleges.models import Fellowship from common.utils import Q_with_alternative_spellings from journals.models import Journal from mails.utils import DirectMailUtil -from preprints.helpers import generate_new_scipost_identifier +from preprints.helpers import get_new_scipost_identifier from preprints.models import Preprint from profiles.models import Profile from scipost.constants import SCIPOST_SUBJECT_AREAS @@ -337,6 +337,70 @@ class SubmissionService: # ###################################################################### +# Checks + +def check_resubmission_readiness(requested_by, submission): + """ + Check if submission can be resubmitted. + """ + if submission: + if 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 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 metadata, identifier + class SubmissionPrefillForm(forms.Form): """ @@ -367,38 +431,7 @@ class SubmissionPrefillForm(forms.Form): """ Consistency checks on the prefill data. """ - self._check_resubmission_readiness() - - def _check_resubmission_readiness(self): - """ - Check if previous submitted versions (if any) can be resubmitted. - """ - if self.latest_submission: - if self.latest_submission.status == STATUS_REJECTED: - # Explicitly give rejected status warning. - error_message = ('This preprint has previously undergone refereeing ' - 'and has been rejected. Resubmission is only possible ' - 'if the manuscript has been substantially reworked into ' - 'a new submission with distinct identifier.') - raise forms.ValidationError(error_message) - elif self.latest_submission.open_for_resubmission: - # Check if verified author list contains current user. - if self.requested_by.contributor not in self.latest_submission.authors.all(): - error_message = ('There exists a preprint with this identifier ' - 'but an earlier version number. Resubmission is only possible' - ' if you are a registered author of this manuscript.') - raise forms.ValidationError(error_message) - else: - # Submission has not an appropriate status for resubmission. - error_message = ('There exists a preprint with this identifier ' - 'but an earlier version number, which is still undergoing ' - 'peer refereeing. ' - 'A resubmission can only be performed after request ' - 'from the Editor-in-charge. Please wait until the ' - 'closing of the previous refereeing round and ' - 'formulation of the Editorial Recommendation ' - 'before proceeding with a resubmission.') - raise forms.ValidationError(error_message) + check_resubmission_readiness(self.requested_by, self.latest_submission) def get_prefill_data(self): return {} @@ -471,31 +504,35 @@ class ArXivPrefillForm(SubmissionPrefillForm): """ identifier = self.cleaned_data.get('arxiv_identifier_w_vn_nr', None) - caller = ArxivCaller(identifier) - if caller.is_valid: - self.arxiv_data = caller.data - self.metadata = caller.metadata - else: - error_message = 'A preprint associated to this identifier does not exist.' - raise forms.ValidationError(error_message) + 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') - - # Check if this paper has already been published (according to arXiv) - 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'] + self.metadata, identifier = check_arxiv_identifier_w_vn_nr(identifier) - 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}) + # caller = ArxivCaller(identifier) + # if caller.is_valid: + # self.arxiv_data = caller.data + # self.metadata = caller.metadata + # else: + # error_message = 'A preprint associated to this identifier does not exist.' + # raise forms.ValidationError(error_message) + + # # 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') + + # # Check if this paper has already been published (according to arXiv) + # 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. ' + # 'It cannot be submitted again.'), + # raise forms.ValidationError(error_message, code='published', + # params={'published_id': published_id}) return identifier def get_prefill_data(self): @@ -583,6 +620,9 @@ class SubmissionForm(forms.ModelForm): self.requested_by = kwargs.pop('requested_by') self.preprint_server = kwargs.pop('preprint_server') self.thread_hash = kwargs['initial'].get('thread_hash', None) + + self.metadata = {} # container for possible external server-provided metadata + # print("form args:\n", args) # print("form kwargs:\n", kwargs) # print("is_resubmission: %s" % self.is_resubmission()) @@ -657,17 +697,22 @@ class SubmissionForm(forms.ModelForm): cleaned_data = super().clean(*args, **kwargs) # SciPost preprints are auto-generated here. - self.scipost_identifier = None + # self.scipost_identifier = None if 'identifier_w_vn_nr' not in cleaned_data: - self.service.identifier, self.scipost_identifier = generate_new_scipost_identifier( - cleaned_data.get('is_resubmission_of', None)) - # Also copy to the form data - self.cleaned_data['identifier_w_vn_nr'] = self.service.identifier + # self.scipost_identifier = generate_new_scipost_identifier( + # cleaned_data.get('is_resubmission_of', None)) + # # Also copy to the form data + # self.cleaned_data['identifier_w_vn_nr'] = self.service.identifier + self.cleaned_data['identifier_w_vn_nr'] = get_new_scipost_identifier( + thread_hash=self.thread_hash) # 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) + # self.service.run_checks() + # self.service.identifier_matches_regex(cleaned_data['submitted_to'].doi_label) + + if self.is_resubmission(): + check_resubmission_readiness(self.requested_by, cleaned_data['is_resubmission_of']) if 'Proc' not in self.cleaned_data['submitted_to'].doi_label: try: @@ -693,6 +738,15 @@ class SubmissionForm(forms.ModelForm): self.add_error('author_list', error_message) return author_list + 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 == 'arXiv': + self.metadata, identifier = check_arxiv_identifier_w_vn_nr(identifier) + return identifier + def clean_submission_type(self): """ Validate Submission type for the SciPost Physics journal. @@ -703,24 +757,6 @@ class SubmissionForm(forms.ModelForm): self.add_error('submission_type', 'Please specify the submission type.') return submission_type - def set_pool(self, submission): - """ - Set the default set of (guest) Fellows for this Submission. - """ - qs = Fellowship.objects.active() - fellows = qs.regular().filter( - contributor__profile__discipline=submission.discipline).filter( - Q(contributor__profile__expertises__contains=[submission.subject_area]) | - Q(contributor__profile__expertises__overlap=submission.secondary_areas) - ).return_active_for_submission(submission) - submission.fellows.set(fellows) - - if submission.proceedings: - # Add Guest Fellowships if the Submission is a Proceedings manuscript - guest_fellows = qs.guests().filter( - proceedings=submission.proceedings).return_active_for_submission(submission) - submission.fellows.add(*guest_fellows) - @transaction.atomic def save(self): """ @@ -736,16 +772,16 @@ class SubmissionForm(forms.ModelForm): identifier_wo_vn_nr=identifiers[0], vn_nr=identifiers[2], url=self.cleaned_data.get('arxiv_link', ''), - scipost_preprint_identifier=self.scipost_identifier, + # scipost_preprint_identifier=self.scipost_identifier, _file=self.cleaned_data.get('preprint_file', None), ) # Save metadata directly from ArXiv call without possible user interception - submission.metadata = self.service.metadata + submission.metadata = self.metadata submission.preprint = preprint submission.save() if self.is_resubmission(): - self.service.process_resubmission_procedure(submission) + self.process_resubmission(submission) # Gather first known author and Fellows. submission.authors.add(self.requested_by.contributor) @@ -755,6 +791,62 @@ class SubmissionForm(forms.ModelForm): 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( + is_current=False, open_for_reporting=False, status=STATUS_RESUBMITTED) + + # Copy Topics + submission.topics.add(*previous_submission.topics.all()) + + # Open for comment and reporting and copy EIC info + Submission.objects.filter(id=submission.id).update( + open_for_reporting=True, + open_for_commenting=True, + visible_pool=True, + refereeing_cycle=CYCLE_UNDETERMINED, + editor_in_charge=previous_submission.editor_in_charge, + status=STATUS_EIC_ASSIGNED) + + # 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=STATUS_ACCEPTED) + + def set_pool(self, submission): + """ + Set the default set of (guest) Fellows for this Submission. + """ + qs = Fellowship.objects.active() + fellows = qs.regular().filter( + contributor__profile__discipline=submission.discipline).filter( + Q(contributor__profile__expertises__contains=[submission.subject_area]) | + Q(contributor__profile__expertises__overlap=submission.secondary_areas) + ).return_active_for_submission(submission) + submission.fellows.set(fellows) + + if submission.proceedings: + # Add Guest Fellowships if the Submission is a Proceedings manuscript + guest_fellows = qs.guests().filter( + proceedings=submission.proceedings).return_active_for_submission(submission) + submission.fellows.add(*guest_fellows) + class SubmissionReportsForm(forms.ModelForm): """Update refereeing pdf for Submission.""" diff --git a/submissions/templates/submissions/submit_choose_preprint_server.html b/submissions/templates/submissions/submit_choose_preprint_server.html index 2c3a16c21..2d8e3f1c8 100644 --- a/submissions/templates/submissions/submit_choose_preprint_server.html +++ b/submissions/templates/submissions/submit_choose_preprint_server.html @@ -49,7 +49,6 @@ </div> <div class="card-body"> {% if server.name == 'SciPost' %} - <a class="btn btn-success text-white" role="button" href="{% url 'submissions:submit_manuscript_scipost' journal_doi_label=journal.doi_label %}{% if thread_hash %}?thread_hash={{ thread_hash }}{% endif %}"><i class="fa fa-arrow-right"></i> Go to the SciPost submission form</a> <form action="{% url 'submissions:submit_manuscript_scipost' journal_doi_label=journal.doi_label %}" method="get"> {{ scipost_prefill_form }} {% if thread_hash %} -- GitLab