diff --git a/commentaries/forms.py b/commentaries/forms.py index 37ec5748cf569596d712534de0a7e1806b654aa5..4e77b521a054eae82330c8b8e2f61a2099e01475 100644 --- a/commentaries/forms.py +++ b/commentaries/forms.py @@ -103,8 +103,9 @@ class RequestCommentaryForm(forms.ModelForm): def save(self, *args, **kwargs): self.instance.parse_links_into_urls() - self.instance.requested_by = self.requested_by - return super().save(self, *args, **kwargs) + if self.requested_by: + self.instance.requested_by = self.requested_by + return super().save(*args, **kwargs) class RequestArxivPreprintForm(RequestCommentaryForm): @@ -240,6 +241,13 @@ class VetCommentaryForm(forms.Form): raise ValueError(('VetCommentaryForm could not be processed ' 'because the data didn\'t validate')) + def clean_refusal_reason(self): + """`refusal_reason` field is required if action==refuse.""" + if self.commentary_is_refused(): + if int(self.cleaned_data['refusal_reason']) == self.REFUSAL_EMPTY: + self.add_error('refusal_reason', 'Please, choose a reason for rejection.') + return self.cleaned_data['refusal_reason'] + def get_commentary(self): """Return Commentary if available""" self._form_is_cleaned() @@ -251,25 +259,23 @@ class VetCommentaryForm(forms.Form): return self.COMMENTARY_REFUSAL_DICT[int(self.cleaned_data['refusal_reason'])] def commentary_is_accepted(self): - self._form_is_cleaned() return int(self.cleaned_data['action_option']) == self.ACTION_ACCEPT def commentary_is_modified(self): - self._form_is_cleaned() return int(self.cleaned_data['action_option']) == self.ACTION_MODIFY def commentary_is_refused(self): - self._form_is_cleaned() return int(self.cleaned_data['action_option']) == self.ACTION_REFUSE def process_commentary(self): """Vet the commentary or delete it from the database""" + # Modified actions are not doing anything. Users are redirected to an edit page instead. if self.commentary_is_accepted(): self.commentary.vetted = True self.commentary.vetted_by = Contributor.objects.get(user=self.user) self.commentary.save() return self.commentary - elif self.commentary_is_modified() or self.commentary_is_refused(): + elif self.commentary_is_refused(): self.commentary.delete() return None diff --git a/commentaries/templates/commentaries/modify_commentary_request.html b/commentaries/templates/commentaries/modify_commentary_request.html new file mode 100644 index 0000000000000000000000000000000000000000..c443012174802059f7e2b35f492ad33aa93dc22c --- /dev/null +++ b/commentaries/templates/commentaries/modify_commentary_request.html @@ -0,0 +1,44 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: vet Commentary requests{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Vet Commentary Page requests</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1>SciPost Commentary Page request to modify and accept:</h1> + </div> +</div> + +<hr> +<div class="row"> + <div class="col-12"> + {% include 'commentaries/_commentary_summary.html' with commentary=commentary %} + </div> +</div> + +<div class="row"> + <div class="col-12"> + <h3 class="mt-4">Abstract:</h3> + <p>{{ commentary.pub_abstract }}</p> + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form action="{% url 'commentaries:modify_commentary_request' commentary_id=commentary.id %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-secondary" value="Submit and accept" /> + </form> + </div> +</div> + +{% endblock %} diff --git a/commentaries/templates/commentaries/request_arxiv_preprint.html b/commentaries/templates/commentaries/request_arxiv_preprint.html index 44ef80f88251608066b436348d7ef430db147308..2c50e631d5561e8f6094c74fa6edecd4fd23144e 100644 --- a/commentaries/templates/commentaries/request_arxiv_preprint.html +++ b/commentaries/templates/commentaries/request_arxiv_preprint.html @@ -8,12 +8,17 @@ <div class="row"> <div class="col-12"> - <h1 class="page-header">Request Activation of a Commentary Page</h1> + <div class="card card-grey"> + <div class="card-block"> + <h1 class="card-title">Request Activation of a Commentary Page</h1> + <a href="{% url 'commentaries:request_published_article' %}">Click here to request a Commentary Page on a published article</a> + </div> + </div> </div> </div> <div class="row"> - <div class="col-12 col-md-8"> + <div class="col-md-8 offset-md-2"> <form action="{% url 'commentaries:prefill_using_arxiv_identifier' %}" method="post"> {% csrf_token %} {{ query_form|bootstrap }} @@ -22,8 +27,9 @@ </div> </div> +{# <hr>#} <div class="row"> - <div class="col-12 col-md-8"> + <div class="col-md-8 offset-md-2"> <form id="requestForm" action="{% url 'commentaries:request_arxiv_preprint' %}" method="post"> {% csrf_token %} {{ form|bootstrap }} diff --git a/commentaries/templates/commentaries/request_commentary.html b/commentaries/templates/commentaries/request_commentary.html index c89649093d40e31bd66aef4d366d9ed2033f5c46..bc71f5b15fc2a196767816fde0e00ee49911a738 100644 --- a/commentaries/templates/commentaries/request_commentary.html +++ b/commentaries/templates/commentaries/request_commentary.html @@ -10,17 +10,15 @@ <div class="row"> <div class="col-12"> - <div class="panel"> - <h1>Request Activation of a Commentary Page:</h1> - </div> + <h1 class="highlight">Request Activation of a Commentary Page:</h1> </div> <div class="col-12"> <ul> <li> - <a href="{% url 'commentaries:request_published_article' %}">Click here to submit a published article</a> + <a href="{% url 'commentaries:request_published_article' %}">Click here to request a Commentary Page on a published article</a> </li> <li> - <a href="{% url 'commentaries:request_arxiv_preprint' %}">Click here to submit an arXiv preprint</a> + <a href="{% url 'commentaries:request_arxiv_preprint' %}">Click here to request a Commentary Page on an arXiv preprint</a> </li> </ul> </div> diff --git a/commentaries/templates/commentaries/request_published_article.html b/commentaries/templates/commentaries/request_published_article.html index b0bf7365a5956c3f77fa1559e83931758fb0fc60..21511e9e5ee37ce74767ee2e92b1b9015dc6885e 100644 --- a/commentaries/templates/commentaries/request_published_article.html +++ b/commentaries/templates/commentaries/request_published_article.html @@ -6,14 +6,20 @@ {% block content %} + <div class="row"> <div class="col-12"> - <h1 class="page-header">Request Activation of a Commentary Page</h1> + <div class="card card-grey"> + <div class="card-block"> + <h1 class="card-title">Request Activation of a Commentary Page</h1> + <a href="{% url 'commentaries:request_arxiv_preprint' %}">Click here to request a Commentary Page on an arXiv preprint</a> + </div> + </div> </div> </div> <div class="row"> - <div class='col-12 col-md-8'> + <div class='col-md-8 offset-md-2'> <form action="{% url 'commentaries:prefill_using_DOI' %}" method="post"> {% csrf_token %} {{ query_form|bootstrap }} @@ -23,7 +29,7 @@ </div> <div class="row"> - <div class='col-12 col-md-8'> + <div class='col-md-8 offset-md-2'> <form id="requestForm" action="{% url 'commentaries:request_published_article' %}" method="post"> {% csrf_token %} {{ form|bootstrap }} @@ -32,4 +38,4 @@ </div> </div> -{% endblock content%} +{% endblock content %} diff --git a/commentaries/templates/commentaries/vet_commentary_requests.html b/commentaries/templates/commentaries/vet_commentary_requests.html index e4f853aa14d57b9ae42e5c24e26ccb5fe56701a7..e6c77048113d89bf60598f70b270c15e25022a67 100644 --- a/commentaries/templates/commentaries/vet_commentary_requests.html +++ b/commentaries/templates/commentaries/vet_commentary_requests.html @@ -15,6 +15,7 @@ <div class="col-12"> {% if not commentary_to_vet %} <h1>There are no Commentary Page requests for you to vet.</h1> + <h3><a href="{% url 'scipost:personal_page' %}">Return to personal page</a></h3> {% else %} <h1>SciPost Commentary Page request to vet:</h1> @@ -27,7 +28,7 @@ </div> <div class="col-md-5"> - <form action="{% url 'commentaries:vet_commentary_request_ack' commentary_id=commentary_to_vet.id %}" method="post"> + <form action="{% url 'commentaries:vet_commentary_requests_submit' commentary_id=commentary_to_vet.id %}" method="post"> {% csrf_token %} {{ form|bootstrap }} <input type="submit" class="btn btn-secondary" value="Submit" /> diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py index 0516fc3af5a9eba0795c1ef6376eb94f9172b5e1..64d6fda33f5bb8bcb968d89dd17567becdbf6bef 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -5,9 +5,10 @@ from django.test import TestCase from common.helpers import model_form_data from scipost.factories import UserFactory, ContributorFactory -from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory, UnvettedArxivPreprintCommentaryFactory -from .forms import RequestCommentaryForm, RequestPublishedArticleForm, VetCommentaryForm, DOIToQueryForm, \ - ArxivQueryForm, RequestArxivPreprintForm +from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory,\ + UnvettedArxivPreprintCommentaryFactory +from .forms import RequestPublishedArticleForm, VetCommentaryForm, DOIToQueryForm,\ + ArxivQueryForm, RequestArxivPreprintForm from .models import Commentary from common.helpers.test import add_groups_and_permissions diff --git a/commentaries/urls.py b/commentaries/urls.py index 03be372fb2a4229fc4ca73cb866c92ba2a56ee1e..7d3280cb2d93dd748b51713f6c7f20cd390a8484 100644 --- a/commentaries/urls.py +++ b/commentaries/urls.py @@ -24,12 +24,15 @@ urlpatterns = [ url(r'^request_commentary$', views.request_commentary, name='request_commentary'), url(r'^request_commentary/published_article$', views.RequestPublishedArticle.as_view(), name='request_published_article'), - url(r'^request_commentary/arxiv_preprint$', views.RequestArxivPreprint.as_view(), name='request_arxiv_preprint'), + url(r'^request_commentary/arxiv_preprint$', views.RequestArxivPreprint.as_view(), + name='request_arxiv_preprint'), url(r'^prefill_using_DOI$', views.prefill_using_DOI, name='prefill_using_DOI'), url(r'^prefill_using_arxiv_identifier$', views.prefill_using_arxiv_identifier, name='prefill_using_arxiv_identifier'), url(r'^vet_commentary_requests$', views.vet_commentary_requests, name='vet_commentary_requests'), - url(r'^vet_commentary_request_ack/(?P<commentary_id>[0-9]+)$', - views.vet_commentary_request_ack, name='vet_commentary_request_ack'), + url(r'^vet_commentary_requests/(?P<commentary_id>[0-9]+)$', views.vet_commentary_requests, + name='vet_commentary_requests_submit'), + url(r'^vet_commentary_requests/(?P<commentary_id>[0-9]+)/modify$', + views.modify_commentary_request, name='modify_commentary_request'), ] diff --git a/commentaries/views.py b/commentaries/views.py index f302df3aa44d335ed92ef0c4a2fb75c12d67ced9..3dc87784707e61820a4f7afc2749d5ddebccb52c 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -4,6 +4,7 @@ from django.contrib.auth.decorators import permission_required from django.core.mail import EmailMessage from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Q +from django.shortcuts import redirect from django.template.loader import render_to_string from django.views.generic.edit import CreateView from django.views.generic.list import ListView @@ -103,22 +104,14 @@ def prefill_using_arxiv_identifier(request): @permission_required('scipost.can_vet_commentary_requests', raise_exception=True) -def vet_commentary_requests(request): +def vet_commentary_requests(request, commentary_id=None): """Show the first commentary thats awaiting vetting""" - contributor = Contributor.objects.get(user=request.user) - commentary_to_vet = (Commentary.objects.awaiting_vetting() - .exclude(requested_by=contributor).first()) # only handle one at a time - form = VetCommentaryForm() - context = {'contributor': contributor, 'commentary_to_vet': commentary_to_vet, 'form': form} - return render(request, 'commentaries/vet_commentary_requests.html', context) - - -@permission_required('scipost.can_vet_commentary_requests', raise_exception=True) -def vet_commentary_request_ack(request, commentary_id): - # Security fix: Smart asses can vet their own commentary without this line. - # Commentary itself not really being used. - get_object_or_404((Commentary.objects.awaiting_vetting() - .exclude(requested_by=request.user.contributor)), id=commentary_id) + queryset = Commentary.objects.awaiting_vetting().exclude(requested_by=request.user.contributor) + if commentary_id: + # Security fix: Smart asses can vet their own commentary without this line. + commentary_to_vet = get_object_or_404(queryset, id=commentary_id) + else: + commentary_to_vet = queryset.first() form = VetCommentaryForm(request.POST or None, user=request.user, commentary_id=commentary_id) if form.is_valid(): @@ -131,21 +124,14 @@ def vet_commentary_request_ack(request, commentary_id): # Retrieve email_template for action if form.commentary_is_accepted(): email_template = 'commentaries/vet_commentary_email_accepted.html' - elif form.commentary_is_modified(): - email_template = 'commentaries/vet_commentary_email_modified.html' - - request_commentary_form = RequestCommentaryForm(initial={ - 'pub_title': commentary.pub_title, - 'arxiv_link': commentary.arxiv_link, - 'pub_DOI_link': commentary.pub_DOI_link, - 'author_list': commentary.author_list, - 'pub_date': commentary.pub_date, - 'pub_abstract': commentary.pub_abstract - }) elif form.commentary_is_refused(): email_template = 'commentaries/vet_commentary_email_rejected.html' email_context['refusal_reason'] = form.get_refusal_reason() email_context['further_explanation'] = form.cleaned_data['email_response_field'] + elif form.commentary_is_modified(): + # For a modified commentary, redirect to request_commentary_form + return redirect(reverse('commentaries:modify_commentary_request', + args=(commentary.id,))) # Send email and process form email_text = render_to_string(email_template, email_context) @@ -159,16 +145,49 @@ def vet_commentary_request_ack(request, commentary_id): emailmessage.send(fail_silently=False) commentary = form.process_commentary() - # For a modified commentary, redirect to request_commentary_form - if form.commentary_is_modified(): - context = {'form': request_commentary_form} - return render(request, 'commentaries/request_commentary.html', context) + messages.success(request, 'SciPost Commentary request vetted.') + return redirect(reverse('commentaries:vet_commentary_requests')) + + context = { + 'commentary_to_vet': commentary_to_vet, + 'form': form + } + return render(request, 'commentaries/vet_commentary_requests.html', context) + + +@permission_required('scipost.can_vet_commentary_requests', raise_exception=True) +def modify_commentary_request(request, commentary_id): + """Modify a commentary request after vetting with status 'modified'.""" + commentary = get_object_or_404((Commentary.objects.awaiting_vetting() + .exclude(requested_by=request.user.contributor)), + id=commentary_id) + form = RequestCommentaryForm(request.POST or None, instance=commentary) + if form.is_valid(): + # Process commentary data + commentary = form.save(commit=False) + commentary.vetted = True + commentary.save() + + # Send email and process form + email_template = 'commentaries/vet_commentary_email_modified.html' + email_text = render_to_string(email_template, {'commentary': commentary}) + email_args = ( + 'SciPost Commentary Page activated', + email_text, + commentary.requested_by.user.email, + ['commentaries@scipost.org'] + ) + emailmessage = EmailMessage(*email_args, reply_to=['commentaries@scipost.org']) + emailmessage.send(fail_silently=False) + + messages.success(request, 'SciPost Commentary request modified and vetted.') + return redirect(reverse('commentaries:vet_commentary_requests')) - context = {'ack_header': 'SciPost Commentary request vetted.', - 'followup_message': 'Return to the ', - 'followup_link': reverse('commentaries:vet_commentary_requests'), - 'followup_link_label': 'Commentary requests page'} - return render(request, 'scipost/acknowledgement.html', context) + context = { + 'commentary': commentary, + 'form': form + } + return render(request, 'commentaries/modify_commentary_request.html', context) class CommentaryListView(ListView): diff --git a/scipost/services.py b/scipost/services.py index 17ca3811f608486fe1a580e636a07591343d1e28..d9a03b5295223b71908cc8d7e97d0753f89ddc1a 100644 --- a/scipost/services.py +++ b/scipost/services.py @@ -80,10 +80,12 @@ class ArxivCaller: def _call_arxiv(self): url = self.query_base_url % self.identifier request = requests.get(url) - arxiv_data = feedparser.parse(request.content)['entries'][0] + response_content = feedparser.parse(request.content) + arxiv_data = response_content['entries'][0] if self._search_result_present(arxiv_data): self.is_valid = True self._arxiv_data = arxiv_data + self.metadata = response_content else: self.is_valid = False @@ -99,9 +101,11 @@ class ArxivCaller: self.data = { 'pub_title': pub_title, + 'title': pub_title, # Duplicate for Commentary/Submission cross-compatibility 'author_list': author_list, 'arxiv_link': arxiv_link, 'pub_abstract': abstract, + 'abstract': abstract, # Duplicate for Commentary/Submission cross-compatibility 'pub_date': pub_date, } diff --git a/strings/__init__.py b/strings/__init__.py index 51115395b4c01978c62b95c036d3f9a5e7445cfc..614455a3452553b019e2f375efa0ad241bd8f690 100644 --- a/strings/__init__.py +++ b/strings/__init__.py @@ -43,11 +43,11 @@ arxiv_caller_errormessages = { 'paper_published_doi': ('This paper has been published under DOI {{ arxiv_doi }}' '. Please comment on the published version.'), - 'arxiv_timeout': 'Arxiv did not respond in time. Please try again later', - 'arxiv_bad_request': - ('There was an error with requesting identifier ' + - '{{ identifier_with_vn_nr }}' - ' from Arxiv. Please check the identifier and try again.'), + # 'arxiv_timeout': 'Arxiv did not respond in time. Please try again later', + # 'arxiv_bad_request': + # ('There was an error with requesting identifier ' + + # '{{ identifier_with_vn_nr }}' + # ' from Arxiv. Please check the identifier and try again.'), 'previous_submission_undergoing_refereeing': ('There exists a preprint with this arXiv identifier ' 'but an earlier version number, which is still undergoing ' diff --git a/submissions/constants.py b/submissions/constants.py index ffb506708de9a0fddee0c601d6e2ca7f97007cf4..845ecbb2ebd49f4117139978bbc36cd0d1b2fc86 100644 --- a/submissions/constants.py +++ b/submissions/constants.py @@ -6,6 +6,8 @@ STATUS_AWAITING_ED_REC = 'awaiting_ed_rec' STATUS_REVIEW_CLOSED = 'review_closed' STATUS_ACCEPTED = 'accepted' STATUS_PUBLISHED = 'published' +STATUS_REJECTED = 'rejected' +STATUS_REJECTED_VISIBLE = 'rejected_visible' STATUS_RESUBMITTED = 'resubmitted' STATUS_RESUBMITTED_REJECTED = 'resubmitted_and_rejected' STATUS_RESUBMITTED_REJECTED_VISIBLE = 'resubmitted_and_rejected_visible' @@ -27,8 +29,8 @@ SUBMISSION_STATUS = ( (STATUS_AWAITING_ED_REC, 'Awaiting Editorial Recommendation'), ('EC_vote_completed', 'Editorial College voting rounded up'), (STATUS_ACCEPTED, 'Publication decision taken: accept'), - ('rejected', 'Publication decision taken: reject'), - ('rejected_visible', 'Publication decision taken: reject (still publicly visible)'), + (STATUS_REJECTED, 'Publication decision taken: reject'), + (STATUS_REJECTED_VISIBLE, 'Publication decision taken: reject (still publicly visible)'), (STATUS_PUBLISHED, 'Published'), # If withdrawn: ('withdrawn', 'Withdrawn by the Authors'), diff --git a/submissions/forms.py b/submissions/forms.py index 70b2b792e8f3465889566fbb4e4ac600b6b59254..c024b162a8ae25f9aee1fa70dd9b0a43371830f6 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -1,16 +1,24 @@ from django import forms +from django.contrib.auth.models import Group from django.core.validators import RegexValidator +from django.db import models, transaction -from .constants import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS,\ - REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES -from .models import Submission, RefereeInvitation, Report, EICRecommendation +from guardian.shortcuts import assign_perm + +from .constants import ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED,\ + REPORT_ACTION_CHOICES, REPORT_REFUSAL_CHOICES, STATUS_REVISION_REQUESTED,\ + STATUS_REJECTED, STATUS_REJECTED_VISIBLE, STATUS_RESUBMISSION_INCOMING +from .models import Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment from scipost.constants import SCIPOST_SUBJECT_AREAS +from scipost.services import ArxivCaller from scipost.models import Contributor from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field, HTML, Submit +import strings + class SubmissionSearchForm(forms.Form): author = forms.CharField(max_length=100, required=False, label="Author(s)") @@ -33,46 +41,157 @@ class SubmissionSearchForm(forms.Form): # Submission and resubmission # ############################### -class SubmissionIdentifierForm(forms.Form): - identifier = forms.CharField( - widget=forms.TextInput( - {'label': 'arXiv identifier', - 'placeholder': 'new style (with version nr) ####.####(#)v#(#)', - 'cols': 20} - ), - validators=[ - RegexValidator( - regex="^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$", - message='The identifier you entered is improperly formatted ' - '(did you forget the version number?)', - code='invalid_identifier' - ), - ]) - - -class SubmissionForm(forms.ModelForm): +class SubmissionChecks: + """ + Use this class as a blueprint containing checks which should be run + in multiple forms. + """ + is_resubmission = False + last_submission = None + + def _submission_already_exists(self, identifier): + if Submission.objects.filter(arxiv_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 _call_arxiv(self, identifier): + caller = ArxivCaller(identifier) + if caller.is_valid: + self.arxiv_data = ArxivCaller(identifier).data + self.metadata = ArxivCaller(identifier).metadata + else: + error_message = 'A preprint associated to this identifier does not exist.' + raise forms.ValidationError(error_message) + + def _submission_is_already_published(self, identifier): + published_id = None + if 'arxiv_doi' in self.arxiv_data: + published_id = self.arxiv_data['arxiv_doi'] + elif 'arxiv_journal_ref' in self.arxiv_data: + published_id = self.arxiv_data['arxiv_journal_ref'] + + if published_id: + error_message = ('This paper has been published under DOI %(published_id)s' + '. Please comment on the published version.'), + raise forms.ValidationError(error_message, code='published', + params={'published_id': published_id}) + + def _submission_previous_version_is_valid_for_submission(self, identifier): + '''Check if previous submitted versions have the appropriate status.''' + identifiers = self.identifier_into_parts(identifier) + submission = (Submission.objects + .filter(arxiv_identifier_wo_vn_nr=identifiers['arxiv_identifier_wo_vn_nr']) + .order_by('-arxiv_vn_nr').last()) + + # If submissions are found; check their statuses + if submission: + self.last_submission = submission + if submission.status == STATUS_REVISION_REQUESTED: + self.is_resubmission = True + elif submission.status in [STATUS_REJECTED, STATUS_REJECTED_VISIBLE]: + error_message = ('This arXiv preprint has previously undergone refereeing ' + 'and has been rejected. Resubmission is only possible ' + 'if the manuscript has been substantially reworked into ' + 'a new arXiv submission with distinct identifier.') + raise forms.ValidationError(error_message) + else: + error_message = ('There exists a preprint with this arXiv identifier ' + 'but an earlier version number, which is still undergoing ' + 'peer refereeing. ' + 'A resubmission can only be performed after request ' + 'from the Editor-in-charge. Please wait until the ' + 'closing of the previous refereeing round and ' + 'formulation of the Editorial Recommendation ' + 'before proceeding with a resubmission.') + raise forms.ValidationError(error_message) + + def submission_is_resubmission(self): + return self.is_resubmission + + def identifier_into_parts(self, identifier): + data = { + 'arxiv_identifier_w_vn_nr': identifier, + 'arxiv_identifier_wo_vn_nr': identifier.rpartition('v')[0], + 'arxiv_vn_nr': int(identifier.rpartition('v')[2]) + } + return data + + def do_pre_checks(self, identifier): + self._submission_already_exists(identifier) + self._call_arxiv(identifier) + self._submission_is_already_published(identifier) + self._submission_previous_version_is_valid_for_submission(identifier) + + +class SubmissionIdentifierForm(SubmissionChecks, forms.Form): + IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$' + IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)' + + identifier = forms.RegexField(regex=IDENTIFIER_PATTERN_NEW, strip=True, + # help_text=strings.arxiv_query_help_text, + error_messages={'invalid': strings.arxiv_query_invalid}, + widget=forms.TextInput({'placeholder': IDENTIFIER_PLACEHOLDER})) + + def clean_identifier(self): + identifier = self.cleaned_data['identifier'] + self.do_pre_checks(identifier) + return identifier + + def _gather_data_from_last_submission(self): + '''Return dictionary with data coming from previous submission version.''' + if self.submission_is_resubmission(): + data = { + 'is_resubmission': True, + 'discipline': self.last_submission.discipline, + 'domain': self.last_submission.domain, + 'referees_flagged': self.last_submission.referees_flagged, + 'referees_suggested': self.last_submission.referees_suggested, + 'secondary_areas': self.last_submission.secondary_areas, + 'subject_area': self.last_submission.subject_area, + 'submitted_to_journal': self.last_submission.submitted_to_journal, + 'submission_type': self.last_submission.submission_type, + } + return data or {} + + def request_arxiv_preprint_form_prefill_data(self): + '''Return dictionary to prefill `RequestSubmissionForm`.''' + form_data = self.arxiv_data + form_data.update(self.identifier_into_parts(self.cleaned_data['identifier'])) + if self.submission_is_resubmission(): + form_data.update(self._gather_data_from_last_submission()) + return form_data + + +class RequestSubmissionForm(SubmissionChecks, forms.ModelForm): class Meta: model = Submission - fields = ['is_resubmission', - 'discipline', 'submitted_to_journal', 'submission_type', - 'domain', 'subject_area', - 'secondary_areas', - 'title', 'author_list', 'abstract', - 'arxiv_identifier_w_vn_nr', 'arxiv_identifier_wo_vn_nr', - 'arxiv_vn_nr', 'arxiv_link', 'metadata', - 'author_comments', 'list_of_changes', - 'remarks_for_editors', - 'referees_suggested', 'referees_flagged'] + fields = [ + 'is_resubmission', + 'discipline', + 'submitted_to_journal', + 'submission_type', + 'domain', + 'subject_area', + 'secondary_areas', + 'title', + 'author_list', + 'abstract', + 'arxiv_identifier_w_vn_nr', + 'arxiv_link', + 'author_comments', + 'list_of_changes', + 'remarks_for_editors', + 'referees_suggested', + 'referees_flagged' + ] def __init__(self, *args, **kwargs): - super(SubmissionForm, self).__init__(*args, **kwargs) + self.requested_by = kwargs.pop('requested_by', None) + super().__init__(*args, **kwargs) self.fields['is_resubmission'].widget = forms.HiddenInput() self.fields['arxiv_identifier_w_vn_nr'].widget = forms.HiddenInput() - self.fields['arxiv_identifier_wo_vn_nr'].widget = forms.HiddenInput() - self.fields['arxiv_vn_nr'].widget = forms.HiddenInput() self.fields['arxiv_link'].widget.attrs.update( {'placeholder': 'ex.: arxiv.org/abs/1234.56789v1'}) - self.fields['metadata'].widget = forms.HiddenInput() self.fields['secondary_areas'].widget = forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS) self.fields['abstract'].widget.attrs.update({'cols': 100}) self.fields['author_comments'].widget.attrs.update({ @@ -88,7 +207,15 @@ class SubmissionForm(forms.ModelForm): 'placeholder': 'Optional: names of referees whose reports should be treated with caution (+ short reason)', 'rows': 3}) - def check_user_may_submit(self, current_user): + def clean(self, *args, **kwargs): + """ + Do all prechecks which are also done in the prefiller. + """ + cleaned_data = super().clean(*args, **kwargs) + self.do_pre_checks(cleaned_data['arxiv_identifier_w_vn_nr']) + return cleaned_data + + def clean_author_list(self): """ Important check! @@ -96,21 +223,87 @@ class SubmissionForm(forms.ModelForm): Also possibly may be extended to check permissions and give ultimate submission power to certain user groups. """ - return current_user.last_name.lower() in self.cleaned_data['author_list'].lower() + author_list = self.cleaned_data['author_list'] + if not self.requested_by.last_name.lower() in author_list.lower(): + error_message = ('Your name does not match that of any of the authors. ' + 'You are not authorized to submit this preprint.') + raise forms.ValidationError(error_message, code='not_an_author') + return author_list + + @transaction.atomic + def copy_and_save_data_from_resubmission(self, submission): + """ + Fill given Submission with data coming from last_submission in the SubmissionChecks + blueprint. + """ + if not self.last_submission: + raise Submission.DoesNotExist + + # Open for comment and reporting + submission.open_for_reporting = True + submission.open_for_commenting = True + + # Close last submission + self.last_submission.is_current = False + self.last_submission.open_for_reporting = False + self.last_submission.status = STATUS_RESUBMITTED + self.last_submission.save() + + # Editor-in-charge + submission.editor_in_charge = self.last_submission.editor_in_charge + submission.status = STATUS_RESUBMISSION_INCOMING + + # Author claim fields + submission.authors.add(*self.last_submission.authors.all()) + submission.authors_claims.add(*self.last_submission.authors_claims.all()) + submission.authors_false_claims.add(*self.last_submission.authors_false_claims.all()) + submission.save() + return submission + + @transaction.atomic + def reassign_eic_and_admins(self, submission): + # Assign permissions + assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) + ed_admins = Group.objects.get(name='Editorial Administrators') + assign_perm('can_take_editorial_actions', ed_admins, submission) + + # Assign editor + assignment = EditorialAssignment( + submission=submission, + to=submission.editor_in_charge, + accepted=True + ) + assignment.save() + submission.save() + return submission - def update_submission_data(self): + @transaction.atomic + def save(self): """ - Some fields should not be accessible in the HTML form by the user and should be - inserted by for example an extra call to Arxiv into the Submission instance, right - *after* the form is submitted. - - Example fields: - - is_resubmission - - arxiv_link - - arxiv_identifier_w_vn_nr - - metadata (!) + Prefill instance before save. + + Because of the ManyToManyField on `authors`, commit=False for this form + is disabled. Saving the form without the database call may loose `authors` + data without notice. """ - raise NotImplementedError + submission = super().save(commit=False) + submission.submitted_by = self.requested_by.contributor + + # Save metadata directly from ArXiv call without possible user interception + submission.metadata = self.metadata + + # Update identifiers + identifiers = self.identifier_into_parts(submission.arxiv_identifier_w_vn_nr) + submission.arxiv_identifier_wo_vn_nr = identifiers['arxiv_identifier_wo_vn_nr'] + submission.arxiv_vn_nr = identifiers['arxiv_vn_nr'] + + # Save + submission.save() + if self.submission_is_resubmission(): + submission = self.copy_and_save_data_from_resubmission(submission) + submission = self.reassign_eic_and_admins(submission) + submission.authors.add(self.requested_by.contributor) + return submission ###################### diff --git a/submissions/models.py b/submissions/models.py index c2364962e70eeff53d55c6d22691d6e965131bb1..9e697af3908fdf117f182ae6a0f26c337a9c900b 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -132,69 +132,6 @@ class Submission(ArxivCallable, models.Model): def reporting_deadline_has_passed(self): return timezone.now() > self.reporting_deadline - @transaction.atomic - def finish_submission(self): - if self.is_resubmission: - # If submissions is a resubmission, the submission needs to be prescreened - # by the EIC to choose which of the available submission cycle to assign - self.mark_other_versions_as_deprecated() - self.copy_authors_from_previous_version() - self.copy_EIC_from_previous_version() - self.set_resubmission_defaults() - self.status = STATUS_RESUBMISSION_INCOMING - else: - self.authors.add(self.submitted_by) - - self.save() - - @classmethod - def same_version_exists(self, identifier): - return self.objects.filter(arxiv_identifier_w_vn_nr=identifier).exists() - - @classmethod - def different_versions(self, identifier): - return self.objects.filter(arxiv_identifier_wo_vn_nr=identifier).order_by('-arxiv_vn_nr') - - def make_assignment(self): - assignment = EditorialAssignment( - submission=self, - to=self.editor_in_charge, - accepted=True, - date_created=timezone.now(), - date_answered=timezone.now(), - ) - assignment.save() - - def set_resubmission_defaults(self): - self.open_for_reporting = True - self.open_for_commenting = True - if self.other_versions()[0].submitted_to_journal == 'SciPost Physics Lecture Notes': - self.reporting_deadline = timezone.now() + datetime.timedelta(days=56) - else: - self.reporting_deadline = timezone.now() + datetime.timedelta(days=28) - - def copy_EIC_from_previous_version(self): - last_version = self.other_versions()[0] - self.editor_in_charge = last_version.editor_in_charge - self.status = 'EICassigned' - - def copy_authors_from_previous_version(self): - last_version = self.other_versions()[0] - - for author in last_version.authors.all(): - self.authors.add(author) - for author in last_version.authors_claims.all(): - self.authors_claims.add(author) - for author in last_version.authors_false_claims.all(): - self.authors_false_claims.add(author) - - def mark_other_versions_as_deprecated(self): - for sub in self.other_versions(): - sub.is_current = False - sub.open_for_reporting = False - sub.status = 'resubmitted' - sub.save() - def other_versions(self): return Submission.objects.filter( arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr diff --git a/submissions/templates/submissions/new_submission.html b/submissions/templates/submissions/new_submission.html index e0dbc27ad7279b3d3b2ead3ff73a8489c311fc5e..7cb8592adf5481cf931fd0d78cdb0119755e5804 100644 --- a/submissions/templates/submissions/new_submission.html +++ b/submissions/templates/submissions/new_submission.html @@ -34,7 +34,14 @@ $(document).ready(function(){ <div class="row"> <div class="col-12"> - <h1 class="highlight">Submit a manuscript to SciPost</h1> + <div class="card card-grey"> + <div class="card-block"> + <h1 class="card-title mb-0">Submit a manuscript to SciPost</h1> + {% if form.arxiv_identifier_w_vn_nr.value %}<h2 class="my-1 py-0 text-blue">{{form.arxiv_identifier_w_vn_nr.value}}{% if form.is_resubmission.value %} <small>(resubmission)</small>{% endif %}</h2>{% endif %} + </div> + </div> + </div> + <div class="col-12"> <p class="mb-1"> Before submitting, make sure you agree with the <a href="{% url 'journals:journals_terms_and_conditions' %}">SciPost Journals Terms and Conditions</a>. </p> diff --git a/submissions/templates/submissions/prefill_using_identifier.html b/submissions/templates/submissions/prefill_using_identifier.html index 5af8f507c3b2805aaff6ce3bb87a12e15cab0ab5..fd3781c6fceba5d9ed060ab85395be4d7cb18e8c 100644 --- a/submissions/templates/submissions/prefill_using_identifier.html +++ b/submissions/templates/submissions/prefill_using_identifier.html @@ -6,35 +6,6 @@ {% block content %} -<script> -$(document).ready(function(){ - $("#id_submission_type").closest('tr').hide() - - $('select#id_submitted_to_journal').on('change', function (){ - var selection = $(this).val(); - switch(selection){ - case "SciPost Physics": - $("#id_submission_type").closest('tr').show() - break; - default: - $("#id_submission_type").closest('tr').hide() - } -}); - - var isresub = $("#id_is_resubmission").val(); - switch(isresub){ - case "True": - $("#id_author_comments").closest('tr').show() - $("#id_list_of_changes").closest('tr').show() - break; - default: - $("#id_author_comments").closest('tr').hide() - $("#id_list_of_changes").closest('tr').hide() - } - -}); -</script> - <div class="row"> <div class="col-12"> <h1 class="highlight">Submit a manuscript to SciPost</h1> @@ -71,11 +42,6 @@ $(document).ready(function(){ </div> </div> - - {% if resubmessage %} - <h3 class="text-success">{{ resubmessage }}</h3> - {% endif %} - {% else %} <h3>You are currently not allowed to submit a manuscript.</h3> {% endif %} diff --git a/submissions/urls.py b/submissions/urls.py index 8465d4f9c7b572bed6e6e1437a7f904c1d377132..bdd6d86f6cc0619472dad96b3bd14996cf081713 100644 --- a/submissions/urls.py +++ b/submissions/urls.py @@ -17,12 +17,9 @@ urlpatterns = [ name='submission_wo_vn_nr'), url(r'^(?P<arxiv_identifier_w_vn_nr>[0-9]{4,}.[0-9]{5,}v[0-9]{1,2})/$', views.submission_detail, name='submission'), - # url(r'^prefill_using_identifier$', - # views.prefill_using_identifier, name='prefill_using_identifier'), - url(r'^prefill_using_identifier$', - views.PrefillUsingIdentifierView.as_view(), name='prefill_using_identifier'), - # url(r'^submit_manuscript$', views.submit_manuscript, name='submit_manuscript'), - url(r'^submit_manuscript$', views.SubmissionCreateView.as_view(), name='submit_manuscript'), + url(r'^submit_manuscript$', views.RequestSubmission.as_view(), name='submit_manuscript'), + url(r'^submit_manuscript/prefill$', views.prefill_using_arxiv_identifier, + name='prefill_using_identifier'), url(r'^pool$', views.pool, name='pool'), url(r'^submissions_by_status/(?P<status>[a-zA-Z_]+)$', views.submissions_by_status, name='submissions_by_status'), diff --git a/submissions/views.py b/submissions/views.py index a3c046f0dca97d4b7c232a9de15eea2490ac9cdd..2aeb15ffceff91f7993542dc718fcf6a4ff2fc42 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -4,22 +4,22 @@ import feedparser from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.models import Group -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, reverse_lazy from django.db import transaction from django.http import Http404 from django.shortcuts import get_object_or_404, render, redirect from django.template import Template, Context from django.utils import timezone +from django.utils.decorators import method_decorator from guardian.decorators import permission_required_or_403 -from guardian.mixins import PermissionRequiredMixin from guardian.shortcuts import assign_perm from .constants import SUBMISSION_STATUS_VOTING_DEPRECATED,\ SUBMISSION_STATUS_PUBLICLY_INVISIBLE, SUBMISSION_STATUS, ED_COMM_CHOICES from .models import Submission, EICRecommendation, EditorialAssignment,\ RefereeInvitation, Report, EditorialCommunication -from .forms import SubmissionIdentifierForm, SubmissionForm, SubmissionSearchForm,\ +from .forms import SubmissionIdentifierForm, RequestSubmissionForm, SubmissionSearchForm,\ RecommendationVoteForm, ConsiderAssignmentForm, AssignSubmissionForm,\ SetRefereeingDeadlineForm, RefereeSelectForm, RefereeRecruitmentForm,\ ConsiderRefereeInvitationForm, EditorialCommunicationForm,\ @@ -27,136 +27,46 @@ from .forms import SubmissionIdentifierForm, SubmissionForm, SubmissionSearchFor SubmissionCycleChoiceForm from .utils import SubmissionUtils -from journals.constants import SCIPOST_JOURNALS_SPECIALIZATIONS from scipost.forms import ModifyPersonalMessageForm, RemarkForm from scipost.models import Contributor, Remark, RegistrationInvitation -from scipost.services import ArxivCaller from scipost.utils import Utils -from strings import arxiv_caller_errormessages_submissions from comments.forms import CommentForm from production.models import ProductionStream -from django.views.generic.edit import CreateView, FormView +from django.views.generic.edit import CreateView from django.views.generic.list import ListView +import strings + ############### # SUBMISSIONS: ############### -class PrefillUsingIdentifierView(PermissionRequiredMixin, FormView): - form_class = SubmissionIdentifierForm - template_name = 'submissions/prefill_using_identifier.html' - permission_required = 'scipost.can_submit_manuscript' - raise_exception = True - - def post(self, request): - identifierform = SubmissionIdentifierForm(request.POST) - if identifierform.is_valid(): - # Use the ArxivCaller class to make the API calls - caller = ArxivCaller(Submission, identifierform.cleaned_data['identifier']) - caller.process() - - if caller.is_valid(): - # Arxiv response is valid and can be shown - - metadata = caller.metadata - is_resubmission = caller.resubmission - title = metadata['entries'][0]['title'] - authorlist = metadata['entries'][0]['authors'][0]['name'] - for author in metadata['entries'][0]['authors'][1:]: - authorlist += ', ' + author['name'] - arxiv_link = metadata['entries'][0]['id'] - abstract = metadata['entries'][0]['summary'] - initialdata = {'is_resubmission': is_resubmission, - 'metadata': metadata, - 'title': title, 'author_list': authorlist, - 'arxiv_identifier_w_vn_nr': caller.identifier_with_vn_nr, - 'arxiv_identifier_wo_vn_nr': caller.identifier_without_vn_nr, - 'arxiv_vn_nr': caller.version_nr, - 'arxiv_link': arxiv_link, 'abstract': abstract} - if is_resubmission: - previous_submissions = caller.previous_submissions - resubmessage = ('There already exists a preprint with this arXiv identifier ' - 'but a different version number. \nYour Submission will be ' - 'handled as a resubmission.') - initialdata['submitted_to_journal'] = previous_submissions[0].submitted_to_journal - initialdata['submission_type'] = previous_submissions[0].submission_type - initialdata['discipline'] = previous_submissions[0].discipline - initialdata['domain'] = previous_submissions[0].domain - initialdata['subject_area'] = previous_submissions[0].subject_area - initialdata['secondary_areas'] = previous_submissions[0].secondary_areas - initialdata['referees_suggested'] = previous_submissions[0].referees_suggested - initialdata['referees_flagged'] = previous_submissions[0].referees_flagged - else: - resubmessage = '' - - form = SubmissionForm(initial=initialdata) - context = {'identifierform': identifierform, - 'form': form, - 'resubmessage': resubmessage} - return render(request, 'submissions/new_submission.html', context) - - else: - msg = caller.get_error_message(arxiv_caller_errormessages_submissions) - identifierform.add_error(None, msg) - return render(request, 'submissions/prefill_using_identifier.html', - {'form': identifierform}) - else: - return render(request, 'submissions/prefill_using_identifier.html', - {'form': identifierform}) - - -class SubmissionCreateView(PermissionRequiredMixin, CreateView): - model = Submission - form_class = SubmissionForm - +@method_decorator(permission_required('scipost.can_submit_manuscript', raise_exception=True), + name='dispatch') +class RequestSubmission(CreateView): + success_url = reverse_lazy('scipost:personal_page') + form_class = RequestSubmissionForm template_name = 'submissions/new_submission.html' - permission_required = 'scipost.can_submit_manuscript' - # Required to use Guardian's CBV PermissionRequiredMixin with a CreateView - # (see https://github.com/django-guardian/django-guardian/pull/433) - permission_object = None - raise_exception = True def get(self, request): - # Only use prefilled forms return redirect('submissions:prefill_using_identifier') + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['requested_by'] = self.request.user + return kwargs + @transaction.atomic def form_valid(self, form): - submitted_by = Contributor.objects.get(user=self.request.user) - form.instance.submitted_by = submitted_by - - # Temporary until moved to new Arxiv Caller - # Check submitting user for authorship ! - # With the new Arxiv caller, this message should already be given in the prefil form! - if not form.check_user_may_submit(self.request.user): - msg = ('Your name does not match that of any of the authors. ' - 'You are not authorized to submit this preprint.') - messages.error(self.request, msg) - return redirect('submissions:prefill_using_identifier') - - # Save all the information contained in the form submission = form.save() + text = ('<h3>Thank you for your Submission to SciPost</h3>' + 'Your Submission will soon be handled by an Editor.') + messages.success(self.request, text) - # Perform all extra actions and set information not contained in the form - submission.finish_submission() - - if submission.is_resubmission: - # Assign permissions - assign_perm('can_take_editorial_actions', submission.editor_in_charge.user, submission) - ed_admins = Group.objects.get(name='Editorial Administrators') - assign_perm('can_take_editorial_actions', ed_admins, submission) - - # Assign editor - assignment = EditorialAssignment( - submission=submission, - to=submission.editor_in_charge, - accepted=True - ) - assignment.save() - + if form.submission_is_resubmission(): # Send emails SubmissionUtils.load({'submission': submission}, self.request) SubmissionUtils.send_authors_resubmission_ack_email() @@ -165,23 +75,40 @@ class SubmissionCreateView(PermissionRequiredMixin, CreateView): # Send emails SubmissionUtils.load({'submission': submission}) SubmissionUtils.send_authors_submission_ack_email() + return super().form_valid(form) + + def form_invalid(self, form): + # r = form.errors + for error_messages in form.errors.values(): + messages.warning(self.request, *error_messages) + return super().form_invalid(form) + + +@permission_required('scipost.can_submit_manuscript', raise_exception=True) +def prefill_using_arxiv_identifier(request): + query_form = SubmissionIdentifierForm(request.POST or None) + if query_form.is_valid(): + prefill_data = query_form.request_arxiv_preprint_form_prefill_data() + form = RequestSubmissionForm(initial=prefill_data) + + # Submit message to user + if query_form.submission_is_resubmission(): + resubmessage = ('There already exists a preprint with this arXiv identifier ' + 'but a different version number. \nYour Submission will be ' + 'handled as a resubmission.') + messages.success(request, resubmessage, fail_silently=True) + else: + messages.success(request, strings.acknowledge_arxiv_query, fail_silently=True) - text = ('<h3>Thank you for your Submission to SciPost</h3>' - 'Your Submission will soon be handled by an Editor.') - messages.success(self.request, text) - return redirect(reverse('scipost:personal_page')) - - def mark_previous_submissions_as_deprecated(self, previous_submissions): - for sub in previous_submissions: - sub.is_current = False - sub.open_for_reporting = False - sub.status = 'resubmitted' - sub.save() + context = { + 'form': form, + } + return render(request, 'submissions/new_submission.html', context) - def previous_submissions(self, form): - return Submission.objects.filter( - arxiv_identifier_wo_vn_nr=form.cleaned_data['arxiv_identifier_wo_vn_nr'] - ) + context = { + 'form': query_form, + } + return render(request, 'submissions/prefill_using_identifier.html', context) class SubmissionListView(ListView):