diff --git a/SciPost_v1/settings/base.py b/SciPost_v1/settings/base.py index 669f3bc7ed10183e7bcea091ff22551cd217818e..3a236b58512d1010200fe03a6e643579614ad7cc 100644 --- a/SciPost_v1/settings/base.py +++ b/SciPost_v1/settings/base.py @@ -93,6 +93,8 @@ INSTALLED_APPS = ( 'submissions', 'theses', 'virtualmeetings', + 'production', + 'partners', 'webpack_loader', ) @@ -121,6 +123,7 @@ SHELL_PLUS_POST_IMPORTS = ( 'VettedCommentaryFactory', 'UnvettedCommentaryFactory', 'UnpublishedVettedCommentaryFactory',)), + ('scipost.factories', ('ContributorFactory')), ) MATHJAX_ENABLED = True diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index f930e4a9312d8e0a5326bac188cb63824c27a412..7b29434e2aae982aa946d1363cfefcf35d4f4d79 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -42,6 +42,9 @@ urlpatterns = [ url(r'^thesis/', include('theses.urls', namespace="theses")), url(r'^meetings/', include('virtualmeetings.urls', namespace="virtualmeetings")), url(r'^news/', include('news.urls', namespace="news")), + url(r'^production/', include('production.urls', namespace="production")), + url(r'^partners/', include('partners.urls', namespace="partners")), + url(r'^supporting_partners/', include('partners.urls', namespace="partners")), # Keep temporarily for historical reasons ] if settings.DEBUG: diff --git a/commentaries/factories.py b/commentaries/factories.py index 85961e640df59ae58a23bf099b0c2a411c0481db..9cac495d97050431bf7ccbacfdd2475cf7c55dcd 100644 --- a/commentaries/factories.py +++ b/commentaries/factories.py @@ -23,6 +23,9 @@ class CommentaryFactory(factory.django.DjangoModelFactory): pub_title = factory.Faker('text') pub_DOI = factory.Sequence(lambda n: random_external_doi()) arxiv_identifier = factory.Sequence(lambda n: random_arxiv_identifier_with_version_number()) + author_list = factory.Faker('name') + pub_abstract = factory.Faker('text') + pub_date = factory.Faker('date') arxiv_link = factory.Faker('uri') pub_abstract = factory.lazy_attribute(lambda x: Faker().paragraph()) @@ -56,3 +59,7 @@ class UnpublishedVettedCommentaryFactory(VettedCommentaryFactory): class UnvettedCommentaryFactory(CommentaryFactory): vetted = False + +class UnvettedArxivPreprintCommentaryFactory(CommentaryFactory): + vetted = False + pub_DOI = None diff --git a/commentaries/forms.py b/commentaries/forms.py index 8d95c58d896ace0b3f84b7b3da68b4b2c6ced1d9..4e77b521a054eae82330c8b8e2f61a2099e01475 100644 --- a/commentaries/forms.py +++ b/commentaries/forms.py @@ -2,107 +2,170 @@ import re from django import forms from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.safestring import mark_safe +from django.template.loader import get_template +from django.template import Context from .models import Commentary +from .constants import COMMENTARY_PUBLISHED, COMMENTARY_PREPRINT +from scipost.services import DOICaller, ArxivCaller from scipost.models import Contributor +import strings + class DOIToQueryForm(forms.Form): - doi = forms.CharField(widget=forms.TextInput( - {'label': 'DOI', 'placeholder': 'ex.: 10.21468/00.000.000000'})) + VALID_DOI_REGEXP = r'^(?i)10.\d{4,9}/[-._;()/:A-Z0-9]+$' + doi = forms.RegexField(regex=VALID_DOI_REGEXP, strip=True, + help_text=strings.doi_query_help_text, + error_messages={'invalid': strings.doi_query_invalid}, + widget=forms.TextInput({ + 'label': 'DOI', + 'placeholder': strings.doi_query_placeholder + })) + + def clean_doi(self): + input_doi = self.cleaned_data['doi'] + + commentary = Commentary.objects.filter(pub_DOI=input_doi) + if commentary.exists(): + error_message = get_template('commentaries/_doi_query_commentary_exists.html').render( + Context({'arxiv_or_DOI_string': commentary[0].arxiv_or_DOI_string}) + ) + raise forms.ValidationError(mark_safe(error_message)) + + caller = DOICaller(input_doi) + if caller.is_valid: + self.crossref_data = DOICaller(input_doi).data + else: + error_message = 'Could not find a resource for that DOI.' + raise forms.ValidationError(error_message) + return input_doi -class IdentifierToQueryForm(forms.Form): - identifier = forms.CharField(widget=forms.TextInput( - {'label': 'arXiv identifier', - 'placeholder': 'new style ####.####(#)v# or old-style e.g. cond-mat/#######'})) + def request_published_article_form_prefill_data(self): + additional_form_data = {'pub_DOI': self.cleaned_data['doi']} + return {**self.crossref_data, **additional_form_data} - def clean(self, *args, **kwargs): - cleaned_data = super(IdentifierToQueryForm, self).clean(*args, **kwargs) - - identifierpattern_new = re.compile("^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$") - identifierpattern_old = re.compile("^[-.a-z]+/[0-9]{7,}v[0-9]{1,2}$") - - if not (identifierpattern_new.match(cleaned_data['identifier']) or - identifierpattern_old.match(cleaned_data['identifier'])): - msg = ('The identifier you entered is improperly formatted ' - '(did you forget the version number?)') - self.add_error('identifier', msg) - - try: - commentary = Commentary.objects.get(arxiv_identifier=cleaned_data['identifier']) - except (Commentary.DoesNotExist, KeyError): - # Commentary either does not exists or form is invalid - commentary = None - - if commentary: - msg = 'There already exists a Commentary Page on this preprint, see %s' % ( - commentary.title_label()) - self.add_error('identifier', msg) - return cleaned_data +class ArxivQueryForm(forms.Form): + IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$' + IDENTIFIER_PATTERN_OLD = r'^[-.a-z]+/[0-9]{7,}v[0-9]{1,2}$' + VALID_ARXIV_IDENTIFIER_REGEX = "(?:{})|(?:{})".format(IDENTIFIER_PATTERN_NEW, IDENTIFIER_PATTERN_OLD) -class RequestCommentaryForm(forms.ModelForm): - """Create new valid Commetary by user request""" - existing_commentary = None + identifier = forms.RegexField(regex=VALID_ARXIV_IDENTIFIER_REGEX, + strip=True, + help_text=strings.arxiv_query_help_text, + error_messages={'invalid': strings.arxiv_query_invalid}, + widget=forms.TextInput({ + 'placeholder': strings.arxiv_query_placeholder})) + + def clean_identifier(self): + identifier = self.cleaned_data['identifier'] + + commentary = Commentary.objects.filter(arxiv_identifier=identifier) + if commentary.exists(): + error_message = get_template('commentaries/_doi_query_commentary_exists.html').render( + Context({'arxiv_or_DOI_string': commentary[0].arxiv_or_DOI_string}) + ) + raise forms.ValidationError(mark_safe(error_message)) + + caller = ArxivCaller(identifier) + if caller.is_valid: + self.arxiv_data = ArxivCaller(identifier).data + else: + error_message = 'Could not find a resource for that arXiv identifier.' + raise forms.ValidationError(error_message) + + return identifier + def request_arxiv_preprint_form_prefill_data(self): + additional_form_data = {'arxiv_identifier': self.cleaned_data['identifier']} + return {**self.arxiv_data, **additional_form_data} + + +class RequestCommentaryForm(forms.ModelForm): class Meta: model = Commentary - fields = ['type', 'discipline', 'domain', 'subject_area', - 'pub_title', 'author_list', - 'metadata', - 'journal', 'volume', 'pages', 'pub_date', - 'arxiv_identifier', - 'pub_DOI', 'pub_abstract'] + fields = [ + 'discipline', 'domain', 'subject_area', 'pub_title', + 'author_list', 'pub_date', 'pub_abstract' + ] + placeholders = { + 'pub_date': 'Format: YYYY-MM-DD' + } def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) - super(RequestCommentaryForm, self).__init__(*args, **kwargs) - self.fields['metadata'].widget = forms.HiddenInput() - self.fields['pub_date'].widget.attrs.update({'placeholder': 'Format: YYYY-MM-DD'}) - self.fields['arxiv_identifier'].widget.attrs.update( - {'placeholder': 'ex.: 1234.56789v1 or cond-mat/1234567v1'}) - self.fields['pub_DOI'].widget.attrs.update({'placeholder': 'ex.: 10.21468/00.000.000000'}) - self.fields['pub_abstract'].widget.attrs.update({'cols': 100}) + self.requested_by = kwargs.pop('requested_by', None) + super().__init__(*args, **kwargs) - def clean(self, *args, **kwargs): - """Check if form is valid and contains an unique identifier""" - cleaned_data = super(RequestCommentaryForm, self).clean(*args, **kwargs) - - # Either Arxiv-ID or DOI is given - if not cleaned_data['arxiv_identifier'] and not cleaned_data['pub_DOI']: - msg = ('You must provide either a DOI (for a published paper) ' - 'or an arXiv identifier (for a preprint).') - self.add_error('arxiv_identifier', msg) - self.add_error('pub_DOI', msg) - elif (cleaned_data['arxiv_identifier'] and - (Commentary.objects - .filter(arxiv_identifier=cleaned_data['arxiv_identifier']).exists())): - msg = 'There already exists a Commentary Page on this preprint, see' - self.existing_commentary = get_object_or_404( - Commentary, - arxiv_identifier=cleaned_data['arxiv_identifier']) - self.add_error('arxiv_identifier', msg) - elif (cleaned_data['pub_DOI'] and - Commentary.objects.filter(pub_DOI=cleaned_data['pub_DOI']).exists()): - msg = 'There already exists a Commentary Page on this publication, see' - self.existing_commentary = get_object_or_404( - Commentary, pub_DOI=cleaned_data['pub_DOI']) - self.add_error('pub_DOI', msg) - - # Current user is not known - if not self.user or not Contributor.objects.filter(user=self.user).exists(): - self.add_error(None, 'Sorry, current user is not known to SciPost.') + def save(self, *args, **kwargs): + self.instance.parse_links_into_urls() + if self.requested_by: + self.instance.requested_by = self.requested_by + return super().save(*args, **kwargs) + + +class RequestArxivPreprintForm(RequestCommentaryForm): + class Meta(RequestCommentaryForm.Meta): + model = Commentary + fields = RequestCommentaryForm.Meta.fields + ['arxiv_identifier'] + + def __init__(self, *args, **kwargs): + super(RequestArxivPreprintForm, self).__init__(*args, **kwargs) + # We want arxiv_identifier to be a required field. + # Since it can be blank on the model, we have to override this property here. + self.fields['arxiv_identifier'].required = True + + # TODO: add regex here? + def clean_arxiv_identifier(self): + arxiv_identifier = self.cleaned_data['arxiv_identifier'] + + commentary = Commentary.objects.filter(arxiv_identifier=arxiv_identifier) + if commentary.exists(): + error_message = get_template('commentaries/_doi_query_commentary_exists.html').render( + Context({'arxiv_or_DOI_string': commentary[0].arxiv_or_DOI_string}) + ) + raise forms.ValidationError(mark_safe(error_message)) + + return arxiv_identifier def save(self, *args, **kwargs): - """Prefill instance before save""" - self.instance.requested_by = Contributor.objects.get(user=self.user) - return super(RequestCommentaryForm, self).save(*args, **kwargs) + self.instance.type = COMMENTARY_PREPRINT + return super().save(*args, **kwargs) + + +class RequestPublishedArticleForm(RequestCommentaryForm): + class Meta(RequestCommentaryForm.Meta): + fields = RequestCommentaryForm.Meta.fields + ['journal', 'volume', 'pages', 'pub_DOI'] + placeholders = { + **RequestCommentaryForm.Meta.placeholders, + **{'pub_DOI': 'ex.: 10.21468/00.000.000000'} + } + + def __init__(self, *args, **kwargs): + super(RequestPublishedArticleForm, self).__init__(*args, **kwargs) + # We want pub_DOI to be a required field. + # Since it can be blank on the model, we have to override this property here. + self.fields['pub_DOI'].required = True - def get_existing_commentary(self): - """Get Commentary if found after validation""" - return self.existing_commentary + def clean_pub_DOI(self): + input_doi = self.cleaned_data['pub_DOI'] + + commentary = Commentary.objects.filter(pub_DOI=input_doi) + if commentary.exists(): + error_message = get_template('commentaries/_doi_query_commentary_exists.html').render( + Context({'arxiv_or_DOI_string': commentary[0].arxiv_or_DOI_string}) + ) + raise forms.ValidationError(mark_safe(error_message)) + + return input_doi + + def save(self, *args, **kwargs): + self.instance.type = COMMENTARY_PUBLISHED + return super().save(*args, **kwargs) class VetCommentaryForm(forms.Form): @@ -178,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() @@ -189,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/models.py b/commentaries/models.py index 51490c7d8fc3c344e9ed2f3104db5a3cb2b80823..da8321b411630469c64122437bb5de53f452a870 100644 --- a/commentaries/models.py +++ b/commentaries/models.py @@ -16,11 +16,11 @@ class Commentary(ArxivCallable, TimeStampedModel): """ A Commentary contains all the contents of a SciPost Commentary page for a given publication. """ - requested_by = models.ForeignKey( - Contributor, blank=True, null=True, - on_delete=models.CASCADE, related_name='requested_by') + requested_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, + on_delete=models.CASCADE, related_name='requested_by') vetted = models.BooleanField(default=False) - vetted_by = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE) + vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True, + on_delete=models.CASCADE) type = models.CharField(max_length=9, choices=COMMENTARY_TYPES) discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default=DISCIPLINE_PHYSICS) diff --git a/commentaries/templates/commentaries/_commentary_summary.html b/commentaries/templates/commentaries/_commentary_summary.html index 681641bf617197f425237dc949a917d76168e69f..be9db8316e26049b3d925a5d52d3886e564b0ccc 100644 --- a/commentaries/templates/commentaries/_commentary_summary.html +++ b/commentaries/templates/commentaries/_commentary_summary.html @@ -11,7 +11,7 @@ <td>As Contributors:</td> <td> {% for author in commentary.authors.all %} - <a href="{% url 'scipost:contributor_info' author.id %}">{{author.user.first_name}} {{author.user.last_name}}</a> + {% if not forloop.first %} · {% endif %}<a href="{% url 'scipost:contributor_info' author.id %}">{{author.user.first_name}} {{author.user.last_name}}</a> {% empty %} (none claimed) {% endfor %} diff --git a/commentaries/templates/commentaries/_doi_query_commentary_exists.html b/commentaries/templates/commentaries/_doi_query_commentary_exists.html new file mode 100644 index 0000000000000000000000000000000000000000..2cc621178767dbf6e36cca6df4d874c0d9703ed1 --- /dev/null +++ b/commentaries/templates/commentaries/_doi_query_commentary_exists.html @@ -0,0 +1 @@ +There already exists a <a href="{% url 'commentaries:commentary' arxiv_or_DOI_string=arxiv_or_DOI_string %}">Commentary Page</a> on this publication. diff --git a/commentaries/templates/commentaries/commentary_list.html b/commentaries/templates/commentaries/commentary_list.html index a8c92a52df708b0f81a43c36bd0b77d6817d63ef..4c1b1269164c93330a273c7137de77a89abc017a 100644 --- a/commentaries/templates/commentaries/commentary_list.html +++ b/commentaries/templates/commentaries/commentary_list.html @@ -71,7 +71,7 @@ {% else %} <h2>Search results:</h3> {% endif %} - {% if object_list %} + {% if commentary_list %} {% if is_paginated %} <p> {% if page_obj.has_previous %} @@ -88,7 +88,7 @@ <div class="col-12"> <ul class="list-group list-group-flush"> - {% for object in object_list %} + {% for object in commentary_list %} <li class="list-group-item"> {% include 'commentaries/_commentary_card_content.html' with commentary=object %} </li> 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 new file mode 100644 index 0000000000000000000000000000000000000000..2c50e631d5561e8f6094c74fa6edecd4fd23144e --- /dev/null +++ b/commentaries/templates/commentaries/request_arxiv_preprint.html @@ -0,0 +1,41 @@ +{% extends 'scipost/base.html' %} +{% load bootstrap %} +{% load scipost_extras %} + +{% block pagetitle %}: request Commentary{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <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-md-8 offset-md-2"> + <form action="{% url 'commentaries:prefill_using_arxiv_identifier' %}" method="post"> + {% csrf_token %} + {{ query_form|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Query arXiv"/> + </form> + </div> +</div> + +{# <hr>#} +<div class="row"> + <div class="col-md-8 offset-md-2"> + <form id="requestForm" action="{% url 'commentaries:request_arxiv_preprint' %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content%} diff --git a/commentaries/templates/commentaries/request_commentary.html b/commentaries/templates/commentaries/request_commentary.html index dec478082ee981283b6a946ff54db43c78383faa..bc71f5b15fc2a196767816fde0e00ee49911a738 100644 --- a/commentaries/templates/commentaries/request_commentary.html +++ b/commentaries/templates/commentaries/request_commentary.html @@ -8,112 +8,19 @@ {% block content %} - <script> - $(document).ready(function(){ - - var allToggableRows = $('#requestForm .form-group').slice(1) - var type_selector = $('select#id_type') - - var preprint = [5,6,7,8,10] - var published = [9] - - function show(indices){ - allToggableRows.each(function(index){ - if($.inArray( index, indices) != -1){ - $(this).hide() - }else{ - $(this).show() - } - - }) - } - - switch (type_selector.val()) { - case "": - allToggableRows.hide() - $("#DOIprefill").hide(); - $("#arXivprefill").hide(); - break; - case "published": - show(published) - $("#DOIprefill").show() - $("#arXivprefill").hide(); - break; - case "preprint": - show(preprint) - $("#DOIprefill").hide() - $("#arXivprefill").show(); - break; - default: - allToggableRows.hide() - $("#DOIprefill").hide(); - $("#arXivprefill").hide(); - } - - type_selector.on('change', function() { - var selection = $(this).val(); - switch(selection){ - case "published": - show(published) - $("#DOIprefill").show() - $("#arXivprefill").hide(); - break; - case "preprint": - show(preprint) - $("#DOIprefill").hide() - $("#arXivprefill").show(); - break; - default: - $("#DOIprefill").hide() - $("#arXivprefill").hide(); - allToggableRows.hide() - } - }); - }); -</script> - <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> -<div class="row"> - <div class="col-md-6 offset-md-3"> - {% if errormessage %} - <h3 style="color: red;">Error: {{ errormessage }}</h3> - {% if existing_commentary %} - <ul>{% include 'commentaries/_commentary_card_content.html' with object=existing_commentary %}</ul> - {% endif %} - <br/> - {% endif %} - - <div id="DOIprefill"> - <h3><em>For published papers, you can prefill the form (except for domain, subject area and abstract) using the DOI:</em></h3> - <p><em>(give the DOI as 10.[4 to 9 digits]/[string], without prefix, as per the placeholder)</em></p> - <form action="{% url 'commentaries:prefill_using_DOI' %}" method="post"> - {% csrf_token %} - {{ doiform|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Query DOI"/> - </form> - </div> - <div id="arXivprefill"> - <h3><em>For preprints, you can prefill the form using the arXiv identifier:</em></h3> - <p><em>(give the identifier without prefix, as per the placeholder)</em></p> - <form action="{% url 'commentaries:prefill_using_identifier' %}" method="post"> - {% csrf_token %} - {{ identifierform|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Query arXiv"/> - </form> - </div> - <br/> - <form id="requestForm" action="{% url 'commentaries:request_commentary' %}" method="post"> - {% csrf_token %} - {{ request_commentary_form|bootstrap }} - <input class="btn btn-primary" type="submit" value="Submit"/> - </form> - + <div class="col-12"> + <ul> + <li> + <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 request a Commentary Page on an arXiv preprint</a> + </li> + </ul> </div> </div> diff --git a/commentaries/templates/commentaries/request_commentary_old.html b/commentaries/templates/commentaries/request_commentary_old.html new file mode 100644 index 0000000000000000000000000000000000000000..e5c38d7f6b93bf7a5ce8a41d050776222d0a2d35 --- /dev/null +++ b/commentaries/templates/commentaries/request_commentary_old.html @@ -0,0 +1,56 @@ +{% extends 'scipost/base.html' %} + +{% load bootstrap %} + +{% load scipost_extras %} + +{% block pagetitle %}: request Commentary{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <div class="panel"> + <h1>Request Activation of a Commentary Page:</h1> + </div> + </div> +</div> +<div class="row"> + <div class="col-md-6 offset-md-3"> + {% if errormessage %} + <h3 style="color: red;">Error: {{ errormessage }}</h3> + {% if existing_commentary %} + <ul>{% include 'commentaries/_commentary_card_content.html' with object=existing_commentary %}</ul> + {% endif %} + <br/> + {% endif %} + + <div id="DOIprefill"> + <h3><em>For published papers, you can prefill the form (except for domain, subject area and abstract) using the DOI:</em></h3> + <p><em>(give the DOI as 10.[4 to 9 digits]/[string], without prefix, as per the placeholder)</em></p> + <form action="{% url 'commentaries:prefill_using_DOI' %}" method="post"> + {% csrf_token %} + {{ doiform|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Query DOI"/> + </form> + </div> + <div id="arXivprefill"> + <h3><em>For preprints, you can prefill the form using the arXiv identifier:</em></h3> + <p><em>(give the identifier without prefix, as per the placeholder)</em></p> + <form action="{% url 'commentaries:prefill_using_identifier' %}" method="post"> + {% csrf_token %} + {{ identifierform|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Query arXiv"/> + </form> + </div> + <br/> + <form id="requestForm" action="{% url 'commentaries:request_commentary' %}" method="post"> + {% csrf_token %} + {{ request_commentary_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + + </div> +</div> + +{% endblock content %} diff --git a/commentaries/templates/commentaries/request_published_article.html b/commentaries/templates/commentaries/request_published_article.html new file mode 100644 index 0000000000000000000000000000000000000000..21511e9e5ee37ce74767ee2e92b1b9015dc6885e --- /dev/null +++ b/commentaries/templates/commentaries/request_published_article.html @@ -0,0 +1,41 @@ +{% extends 'scipost/base.html' %} +{% load bootstrap %} +{% load scipost_extras %} + +{% block pagetitle %}: request Commentary{% endblock pagetitle %} + +{% block content %} + + +<div class="row"> + <div class="col-12"> + <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-md-8 offset-md-2'> + <form action="{% url 'commentaries:prefill_using_DOI' %}" method="post"> + {% csrf_token %} + {{ query_form|bootstrap }} + <input class="btn btn-secondary" type="submit" value="Query DOI"/> + </form> + </div> +</div> + +<div class="row"> + <div class='col-md-8 offset-md-2'> + <form id="requestForm" action="{% url 'commentaries:request_published_article' %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/commentaries/templates/commentaries/vet_commentary_requests.html b/commentaries/templates/commentaries/vet_commentary_requests.html index 70edddb6e8eaca847e8337f0d5b1d003e759daae..e6c77048113d89bf60598f70b270c15e25022a67 100644 --- a/commentaries/templates/commentaries/vet_commentary_requests.html +++ b/commentaries/templates/commentaries/vet_commentary_requests.html @@ -1,35 +1,41 @@ -{% extends 'scipost/base.html' %} +{% extends 'scipost/_personal_page_base.html' %} -{% block pagetitle %}: vet Commentary requests{% endblock pagetitle %} - -{% block bodysup %} - -<section> - {% if not commentary_to_vet %} - <h1>There are no Commentary Page requests for you to vet.</h1> - - {% else %} +{% load bootstrap %} - <h1>SciPost Commentary Page request to vet:</h1> - - <br> - <hr> - <div class="row"> - <div class="col-8"> - {% include 'commentaries/_commentary_summary.html' with commentary=commentary_to_vet %} - <h3>Abstract:</h3> - <p>{{ commentary_to_vet.pub_abstract }}</p> +{% 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"> + {% 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> + + <hr> + <div class="row"> + <div class="col-md-7"> + {% include 'commentaries/_commentary_summary.html' with commentary=commentary_to_vet %} + <h3 class="mt-4">Abstract:</h3> + <p>{{ commentary_to_vet.pub_abstract }}</p> + + </div> + <div class="col-md-5"> + <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" /> + </div> + </div> + {% endif %} </div> - <div class="col-4"> - <form action="{% url 'commentaries:vet_commentary_request_ack' commentary_id=commentary_to_vet.id %}" method="post"> - {% csrf_token %} - {{ form.as_ul }} - <input type="submit" value="Submit" /> - </div> - </div> - - {% endif %} -</section> +</div> -{% endblock bodysup %} +{% endblock %} diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py index 8733064ae2267d07111ed7c9fe0c1662cc85fb3f..64d6fda33f5bb8bcb968d89dd17567becdbf6bef 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -1,17 +1,105 @@ +import re + from django.test import TestCase from common.helpers import model_form_data -from scipost.factories import UserFactory +from scipost.factories import UserFactory, ContributorFactory -from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory -from .forms import RequestCommentaryForm, VetCommentaryForm +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 +class TestArxivQueryForm(TestCase): + def setUp(self): + add_groups_and_permissions() + ContributorFactory.create_batch(5) + + def test_new_arxiv_identifier_is_valid(self): + new_identifier_data = {'identifier': '1612.07611v1'} + form = ArxivQueryForm(new_identifier_data) + self.assertTrue(form.is_valid()) + + def test_old_arxiv_identifier_is_valid(self): + old_identifier_data = {'identifier': 'cond-mat/0612480v1'} + form = ArxivQueryForm(old_identifier_data) + self.assertTrue(form.is_valid()) + + def test_invalid_arxiv_identifier(self): + invalid_data = {'identifier': 'i am not valid'} + form = ArxivQueryForm(invalid_data) + self.assertFalse(form.is_valid()) + + def test_new_arxiv_identifier_without_version_number_is_invalid(self): + data = {'identifier': '1612.07611'} + form = ArxivQueryForm(data) + self.assertFalse(form.is_valid()) + + def test_old_arxiv_identifier_without_version_number_is_invalid(self): + data = {'identifier': 'cond-mat/0612480'} + form = ArxivQueryForm(data) + self.assertFalse(form.is_valid()) + + def test_arxiv_identifier_that_already_has_commentary_page_is_invalid(self): + unvetted_commentary = UnvettedCommentaryFactory() + invalid_data = {'identifier': unvetted_commentary.arxiv_identifier} + form = ArxivQueryForm(invalid_data) + self.assertFalse(form.is_valid()) + error_message = form.errors['identifier'][0] + self.assertRegexpMatches(error_message, re.compile('already exist')) + + def test_valid_but_non_existent_identifier_is_invalid(self): + invalid_data = {'identifier': '1613.07611v1'} + form = ArxivQueryForm(invalid_data) + self.assertFalse(form.is_valid()) + + +class TestDOIToQueryForm(TestCase): + def setUp(self): + add_groups_and_permissions() + ContributorFactory.create_batch(5) + + def test_invalid_doi_is_invalid(self): + invalid_data = {'doi': 'blablab'} + form = DOIToQueryForm(invalid_data) + self.assertFalse(form.is_valid()) + + def test_doi_that_already_has_commentary_page_is_invalid(self): + unvetted_commentary = UnvettedCommentaryFactory() + invalid_data = {'doi': unvetted_commentary.pub_DOI} + form = DOIToQueryForm(invalid_data) + self.assertFalse(form.is_valid()) + error_message = form.errors['doi'][0] + self.assertRegexpMatches(error_message, re.compile('already exist')) + + def test_physrev_doi_is_valid(self): + physrev_doi = "10.21468/SciPostPhys.2.2.010" + form = DOIToQueryForm({'doi': physrev_doi}) + self.assertTrue(form.is_valid()) + + def test_scipost_doi_is_valid(self): + scipost_doi = "10.21468/SciPostPhys.2.2.010" + form = DOIToQueryForm({'doi': scipost_doi}) + self.assertTrue(form.is_valid()) + + def test_old_doi_is_valid(self): + old_doi = "10.1088/0022-3719/7/6/005" + form = DOIToQueryForm({'doi': old_doi}) + self.assertTrue(form.is_valid()) + + def test_valid_but_nonexistent_doi_is_invalid(self): + doi = "10.21468/NonExistentJournal.2.2.010" + form = DOIToQueryForm({'doi': doi}) + self.assertEqual(form.is_valid(), False) + + class TestVetCommentaryForm(TestCase): def setUp(self): add_groups_and_permissions() + ContributorFactory.create_batch(5) self.commentary = UnvettedCommentaryFactory.create() self.user = UserFactory() self.form_data = { @@ -20,6 +108,7 @@ class TestVetCommentaryForm(TestCase): 'email_response_field': 'Lorem Ipsum' } + def test_valid_accepted_form(self): """Test valid form data and return Commentary""" form = VetCommentaryForm(self.form_data, commentary_id=self.commentary.id, user=self.user) @@ -70,71 +159,55 @@ class TestVetCommentaryForm(TestCase): self.assertRaises(ValueError, form.process_commentary) -class TestRequestCommentaryForm(TestCase): +class TestRequestPublishedArticleForm(TestCase): def setUp(self): add_groups_and_permissions() - factory_instance = VettedCommentaryFactory.build() + ContributorFactory.create_batch(5) + factory_instance = UnvettedCommentaryFactory.build() self.user = UserFactory() - self.valid_form_data = model_form_data(factory_instance, RequestCommentaryForm) - - def empty_and_return_form_data(self, key): - """Empty specific valid_form_data field and return""" - self.valid_form_data[key] = None - return self.valid_form_data - - def test_valid_data_is_valid_for_arxiv(self): - """Test valid form for Arxiv identifier""" - form_data = self.valid_form_data - form_data['pub_DOI'] = '' - form = RequestCommentaryForm(form_data, user=self.user) - self.assertTrue(form.is_valid()) - - # Check if the user is properly saved to the new Commentary as `requested_by` - commentary = form.save() - self.assertTrue(commentary.requested_by) + self.valid_form_data = model_form_data(factory_instance, RequestPublishedArticleForm) - def test_valid_data_is_valid_for_DOI(self): + def test_valid_data_is_valid(self): """Test valid form for DOI""" - form_data = self.valid_form_data - form_data['arxiv_identifier'] = '' - form = RequestCommentaryForm(form_data, user=self.user) + form = RequestPublishedArticleForm(self.valid_form_data) self.assertTrue(form.is_valid()) - def test_form_has_no_identifiers(self): - """Test invalid form has no DOI nor Arxiv ID""" - form_data = self.valid_form_data - form_data['pub_DOI'] = '' - form_data['arxiv_identifier'] = '' - form = RequestCommentaryForm(form_data, user=self.user) - self.assertFalse(form.is_valid()) - self.assertTrue('arxiv_identifier' in form.errors) - self.assertTrue('pub_DOI' in form.errors) - - def test_form_with_duplicate_DOI(self): - """Test form response with already existing DOI""" - # Create a factory instance containing Arxiv ID and DOI - VettedCommentaryFactory.create() - - # Test duplicate DOI entry - form_data = self.empty_and_return_form_data('arxiv_identifier') - form = RequestCommentaryForm(form_data, user=self.user) - self.assertTrue('pub_DOI' in form.errors) - self.assertFalse(form.is_valid()) + def test_doi_that_already_has_commentary_page_is_invalid(self): + unvetted_commentary = UnvettedCommentaryFactory() + invalid_data = {**self.valid_form_data, **{'pub_DOI': unvetted_commentary.pub_DOI}} + form = RequestPublishedArticleForm(invalid_data) + self.assertEqual(form.is_valid(), False) + error_message = form.errors['pub_DOI'][0] + self.assertRegexpMatches(error_message, re.compile('already exist')) - # Check is existing commentary is valid - existing_commentary = form.get_existing_commentary() - self.assertEqual(existing_commentary.pub_DOI, form_data['pub_DOI']) + def test_commentary_without_pub_DOI_is_invalid(self): + invalid_data = {**self.valid_form_data, **{'pub_DOI': ''}} + form = RequestPublishedArticleForm(invalid_data) + self.assertEqual(form.is_valid(), False) - def test_form_with_duplicate_arxiv_id(self): - """Test form response with already existing Arxiv ID""" - VettedCommentaryFactory.create() - # Test duplicate Arxiv entry - form_data = self.empty_and_return_form_data('pub_DOI') - form = RequestCommentaryForm(form_data, user=self.user) - self.assertTrue('arxiv_identifier' in form.errors) - self.assertFalse(form.is_valid()) +class TestRequestArxivPreprintForm(TestCase): + def setUp(self): + add_groups_and_permissions() + ContributorFactory.create_batch(5) + factory_instance = UnvettedArxivPreprintCommentaryFactory.build() + self.user = UserFactory() + self.valid_form_data = model_form_data(factory_instance, RequestPublishedArticleForm) + self.valid_form_data['arxiv_identifier'] = factory_instance.arxiv_identifier + + def test_valid_data_is_valid(self): + form = RequestArxivPreprintForm(self.valid_form_data) + self.assertTrue(form.is_valid()) - # Check is existing commentary is valid - existing_commentary = form.get_existing_commentary() - self.assertEqual(existing_commentary.arxiv_identifier, form_data['arxiv_identifier']) + def test_identifier_that_already_has_commentary_page_is_invalid(self): + commentary = UnvettedArxivPreprintCommentaryFactory() + invalid_data = {**self.valid_form_data, **{'arxiv_identifier': commentary.arxiv_identifier}} + form = RequestArxivPreprintForm(invalid_data) + self.assertEqual(form.is_valid(), False) + error_message = form.errors['arxiv_identifier'][0] + self.assertRegexpMatches(error_message, re.compile('already exist')) + + def test_commentary_without_arxiv_identifier_is_invalid(self): + invalid_data = {**self.valid_form_data, **{'arxiv_identifier': ''}} + form = RequestArxivPreprintForm(invalid_data) + self.assertEqual(form.is_valid(), False) diff --git a/commentaries/test_models.py b/commentaries/test_models.py index e4defabc8afbec48d89189ef98a5e9697d1657d0..5fb96f3ef5ac04ae31bf79dd6b63da465137836e 100644 --- a/commentaries/test_models.py +++ b/commentaries/test_models.py @@ -1 +1,11 @@ -# from django.test import TestCase +from django.test import TestCase + +from common.helpers.test import add_groups_and_permissions + +from scipost.factories import ContributorFactory +from .factories import UnvettedCommentaryFactory + + +class TestCommentary(TestCase): + def setUp(self): + add_groups_and_permissions() diff --git a/commentaries/test_views.py b/commentaries/test_views.py index 17ea43b4c1d38d48a40b41d0e9edddd86930d61a..24f46518623509e53016c7939f564ceee26d53cd 100644 --- a/commentaries/test_views.py +++ b/commentaries/test_views.py @@ -2,38 +2,79 @@ from django.core.urlresolvers import reverse from django.contrib.auth.models import Group from django.test import TestCase, Client, RequestFactory +from scipost.models import Contributor from scipost.factories import ContributorFactory, UserFactory -from .factories import UnvettedCommentaryFactory, VettedCommentaryFactory, UnpublishedVettedCommentaryFactory -from .forms import CommentarySearchForm +from .factories import UnvettedCommentaryFactory, VettedCommentaryFactory, UnpublishedVettedCommentaryFactory, \ + UnvettedArxivPreprintCommentaryFactory +from .forms import CommentarySearchForm, RequestPublishedArticleForm from .models import Commentary -from .views import RequestCommentary +from .views import RequestPublishedArticle, prefill_using_DOI, RequestArxivPreprint from common.helpers.test import add_groups_and_permissions +from common.helpers import model_form_data -class RequestCommentaryTest(TestCase): - """Test cases for `request_commentary` view method""" +class PrefillUsingDOITest(TestCase): def setUp(self): add_groups_and_permissions() - self.view_url = reverse('commentaries:request_commentary') - self.login_url = reverse('scipost:login') - self.redirected_login_url = '%s?next=%s' % (self.login_url, self.view_url) - - def test_redirects_if_not_logged_in(self): - request = self.client.get(self.view_url) - self.assertRedirects(request, self.redirected_login_url) + self.target = reverse('commentaries:prefill_using_DOI') + self.physrev_doi = '10.1103/PhysRevB.92.214427' - def test_valid_response_if_logged_in(self): - """Test different GET requests on view""" - request = RequestFactory().get(self.view_url) + def test_submit_valid_physrev_doi(self): + post_data = {'doi': self.physrev_doi} + request = RequestFactory().post(self.target, post_data) request.user = UserFactory() - response = RequestCommentary.as_view()(request) + + response = prefill_using_DOI(request) self.assertEqual(response.status_code, 200) - def test_post_invalid_forms(self): - """Test different kind of invalid RequestCommentaryForm submits""" - raise NotImplementedError +class RequestPublishedArticleTest(TestCase): + def setUp(self): + add_groups_and_permissions() + self.target = reverse('commentaries:request_published_article') + self.contributor = ContributorFactory() + self.commentary_instance = UnvettedCommentaryFactory.build(requested_by=self.contributor) + self.valid_form_data = model_form_data(self.commentary_instance, RequestPublishedArticleForm) + + def test_commentary_gets_created_with_correct_type_and_link(self): + request = RequestFactory().post(self.target, self.valid_form_data) + request.user = self.contributor.user + + self.assertEqual(Commentary.objects.count(), 0) + response = RequestPublishedArticle.as_view()(request) + self.assertEqual(Commentary.objects.count(), 1) + + commentary = Commentary.objects.first() + self.assertEqual(commentary.pub_DOI, self.valid_form_data['pub_DOI']) + self.assertEqual(commentary.type, 'published') + self.assertEqual(commentary.arxiv_or_DOI_string, commentary.pub_DOI) + self.assertEqual(commentary.requested_by, self.contributor) + + +class RequestArxivPreprintTest(TestCase): + def setUp(self): + add_groups_and_permissions() + self.target = reverse('commentaries:request_arxiv_preprint') + self.contributor = ContributorFactory() + self.commentary_instance = UnvettedArxivPreprintCommentaryFactory.build(requested_by=self.contributor) + self.valid_form_data = model_form_data(self.commentary_instance, RequestPublishedArticleForm) + # The form field is called 'identifier', while the model field is called 'arxiv_identifier', + # so model_form_data doesn't include it. + self.valid_form_data['arxiv_identifier'] = self.commentary_instance.arxiv_identifier + + def test_commentary_gets_created_with_correct_type_and_link_and_requested_by(self): + request = RequestFactory().post(self.target, self.valid_form_data) + request.user = self.contributor.user + + self.assertEqual(Commentary.objects.count(), 0) + response = RequestArxivPreprint.as_view()(request) + self.assertEqual(Commentary.objects.count(), 1) + commentary = Commentary.objects.first() + self.assertEqual(commentary.arxiv_identifier, self.valid_form_data['arxiv_identifier']) + self.assertEqual(commentary.type, 'preprint') + self.assertEqual(commentary.arxiv_or_DOI_string, "arXiv:" + self.commentary_instance.arxiv_identifier) + self.assertEqual(commentary.requested_by, self.contributor) class VetCommentaryRequestsTest(TestCase): """Test cases for `vet_commentary_requests` view method""" @@ -77,12 +118,13 @@ class VetCommentaryRequestsTest(TestCase): self.assertEquals(response.context['commentary_to_vet'], None) # Only vetted Commentaries exist! - VettedCommentaryFactory() + # ContributorFactory.create_batch(5) + VettedCommentaryFactory(requested_by=ContributorFactory(), vetted_by=ContributorFactory()) response = self.client.get(self.view_url) self.assertEquals(response.context['commentary_to_vet'], None) # Unvetted Commentaries do exist! - UnvettedCommentaryFactory() + UnvettedCommentaryFactory(requested_by=ContributorFactory()) response = self.client.get(self.view_url) self.assertTrue(type(response.context['commentary_to_vet']) is Commentary) @@ -92,7 +134,7 @@ class BrowseCommentariesTest(TestCase): def setUp(self): add_groups_and_permissions() - VettedCommentaryFactory(discipline='physics') + VettedCommentaryFactory(discipline='physics', requested_by=ContributorFactory()) self.view_url = reverse('commentaries:browse', kwargs={ 'discipline': 'physics', 'nrweeksback': '1' @@ -104,7 +146,7 @@ class BrowseCommentariesTest(TestCase): self.assertEquals(response.status_code, 200) # The created vetted Commentary is found! - self.assertTrue(response.context['commentary_browse_list'].count() >= 1) + self.assertTrue(response.context['commentary_list'].count() >= 1) # The search form is passed trough the view... self.assertTrue(type(response.context['form']) is CommentarySearchForm) @@ -113,7 +155,8 @@ class CommentaryDetailTest(TestCase): def setUp(self): add_groups_and_permissions() self.client = Client() - self.commentary = UnpublishedVettedCommentaryFactory() + self.commentary = UnpublishedVettedCommentaryFactory( + requested_by=ContributorFactory(), vetted_by=ContributorFactory()) self.target = reverse( 'commentaries:commentary', kwargs={'arxiv_or_DOI_string': self.commentary.arxiv_or_DOI_string} @@ -122,3 +165,9 @@ class CommentaryDetailTest(TestCase): def test_status_code_200(self): response = self.client.get(self.target) self.assertEqual(response.status_code, 200) + + def test_unvetted_commentary(self): + commentary = UnvettedCommentaryFactory(requested_by=ContributorFactory()) + target = reverse('commentaries:commentary', kwargs={'arxiv_or_DOI_string': commentary.arxiv_or_DOI_string}) + response = self.client.get(target) + self.assertEqual(response.status_code, 404) diff --git a/commentaries/urls.py b/commentaries/urls.py index a1b5ba5ec119853ae337ab52c7213cb4feadea8f..7d3280cb2d93dd748b51713f6c7f20cd390a8484 100644 --- a/commentaries/urls.py +++ b/commentaries/urls.py @@ -21,12 +21,18 @@ urlpatterns = [ url(r'^(?P<arxiv_or_DOI_string>arXiv:[a-z-]+/[0-9]{7,}(v[0-9]+)?)/$', views.commentary_detail, name='commentary'), - url(r'^request_commentary$', views.RequestCommentary.as_view(), name='request_commentary'), + 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'^prefill_using_DOI$', views.prefill_using_DOI, name='prefill_using_DOI'), - url(r'^prefill_using_identifier$', views.PrefillUsingIdentifierView.as_view(), - name='prefill_using_identifier'), + 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 d8554ec92e92690cf12c7c6e27802183d3f2154a..3dc87784707e61820a4f7afc2749d5ddebccb52c 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -1,275 +1,200 @@ -import re -import requests - from django.shortcuts import get_object_or_404, render from django.contrib import messages from django.contrib.auth.decorators import permission_required -from django.contrib.auth.mixins import LoginRequiredMixin 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, FormView +from django.views.generic.edit import CreateView from django.views.generic.list import ListView from django.utils.decorators import method_decorator +from django.http import Http404 from .models import Commentary -from .forms import RequestCommentaryForm, DOIToQueryForm, IdentifierToQueryForm -from .forms import VetCommentaryForm, CommentarySearchForm +from .forms import DOIToQueryForm, ArxivQueryForm, VetCommentaryForm, RequestCommentaryForm,\ + CommentarySearchForm, RequestPublishedArticleForm, RequestArxivPreprintForm from comments.models import Comment from comments.forms import CommentForm from scipost.models import Contributor -from scipost.services import ArxivCaller import strings -################ -# Commentaries -################ - -class RequestCommentaryMixin(object): - def get_context_data(self, **kwargs): - '''Pass the DOI and identifier forms to the context.''' - if 'request_commentary_form' not in kwargs: - # Only intercept if not prefilled - kwargs['request_commentary_form'] = RequestCommentaryForm() - context = super(RequestCommentaryMixin, self).get_context_data(**kwargs) - - context['existing_commentary'] = None - context['doiform'] = DOIToQueryForm() - context['identifierform'] = IdentifierToQueryForm() - return context +@permission_required('scipost.can_request_commentary_pages', raise_exception=True) +def request_commentary(request): + return render(request, 'commentaries/request_commentary.html') @method_decorator(permission_required( 'scipost.can_request_commentary_pages', raise_exception=True), name='dispatch') -class RequestCommentary(LoginRequiredMixin, RequestCommentaryMixin, CreateView): - form_class = RequestCommentaryForm - template_name = 'commentaries/request_commentary.html' +class RequestCommentary(CreateView): success_url = reverse_lazy('scipost:personal_page') - def get_form_kwargs(self, *args, **kwargs): - '''User should be included in the arguments to have a valid form.''' - form_kwargs = super(RequestCommentary, self).get_form_kwargs(*args, **kwargs) - form_kwargs['user'] = self.request.user - return form_kwargs + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs['requested_by'] = self.request.user.contributor + return kwargs def form_valid(self, form): - form.instance.parse_links_into_urls() - messages.success(self.request, strings.acknowledge_request_commentary) - return super(RequestCommentary, self).form_valid(form) + messages.success(self.request, strings.acknowledge_request_commentary, fail_silently=True) + return super().form_valid(form) + + +class RequestPublishedArticle(RequestCommentary): + form_class = RequestPublishedArticleForm + template_name = 'commentaries/request_published_article.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['query_form'] = DOIToQueryForm() + return context + + +class RequestArxivPreprint(RequestCommentary): + form_class = RequestArxivPreprintForm + template_name = 'commentaries/request_arxiv_preprint.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['query_form'] = ArxivQueryForm() + return context @permission_required('scipost.can_request_commentary_pages', raise_exception=True) def prefill_using_DOI(request): - """ Probes CrossRef API with the DOI, to pre-fill the form. """ if request.method == "POST": - doiform = DOIToQueryForm(request.POST) - if doiform.is_valid(): - # Check if given doi is of expected form: - doipattern = re.compile("^10.[0-9]{4,9}/[-._;()/:a-zA-Z0-9]+") - errormessage = '' - existing_commentary = None - if not doipattern.match(doiform.cleaned_data['doi']): - errormessage = 'The DOI you entered is improperly formatted.' - elif Commentary.objects.filter(pub_DOI=doiform.cleaned_data['doi']).exists(): - errormessage = 'There already exists a Commentary Page on this publication, see' - existing_commentary = get_object_or_404(Commentary, - pub_DOI=doiform.cleaned_data['doi']) - if errormessage: - form = RequestCommentaryForm() - identifierform = IdentifierToQueryForm() - context = { - 'request_commentary_form': form, - 'doiform': doiform, - 'identifierform': identifierform, - 'errormessage': errormessage, - 'existing_commentary': existing_commentary} - return render(request, 'commentaries/request_commentary.html', context) - - # Otherwise we query Crossref for the information: - try: - queryurl = 'http://api.crossref.org/works/%s' % doiform.cleaned_data['doi'] - doiquery = requests.get(queryurl) - doiqueryJSON = doiquery.json() - metadata = doiqueryJSON - pub_title = doiqueryJSON['message']['title'][0] - authorlist = (doiqueryJSON['message']['author'][0]['given'] + ' ' + - doiqueryJSON['message']['author'][0]['family']) - for author in doiqueryJSON['message']['author'][1:]: - authorlist += ', ' + author['given'] + ' ' + author['family'] - journal = doiqueryJSON['message']['container-title'][0] - - try: - volume = doiqueryJSON['message']['volume'] - except KeyError: - volume = '' - - pages = '' - try: - pages = doiqueryJSON['message']['article-number'] # for Phys Rev - except KeyError: - pass - try: - pages = doiqueryJSON['message']['page'] - except KeyError: - pass - - pub_date = '' - try: - pub_date = (str(doiqueryJSON['message']['issued']['date-parts'][0][0]) + '-' + - str(doiqueryJSON['message']['issued']['date-parts'][0][1])) - try: - pub_date += '-' + str( - doiqueryJSON['message']['issued']['date-parts'][0][2]) - except (IndexError, KeyError): - pass - except (IndexError, KeyError): - pass - pub_DOI = doiform.cleaned_data['doi'] - form = RequestCommentaryForm( - initial={'type': 'published', 'metadata': metadata, - 'pub_title': pub_title, 'author_list': authorlist, - 'journal': journal, 'volume': volume, - 'pages': pages, 'pub_date': pub_date, - 'pub_DOI': pub_DOI}) - identifierform = IdentifierToQueryForm() - context = { - 'request_commentary_form': form, - 'doiform': doiform, - 'identifierform': identifierform - } - context['title'] = pub_title - return render(request, 'commentaries/request_commentary.html', context) - except (IndexError, KeyError, ValueError): - pass + query_form = DOIToQueryForm(request.POST) + # The form checks if doi is valid and commentary doesn't already exist. + if query_form.is_valid(): + prefill_data = query_form.request_published_article_form_prefill_data() + form = RequestPublishedArticleForm(initial=prefill_data) + messages.success(request, strings.acknowledge_doi_query, fail_silently=True) else: - pass - return redirect(reverse('commentaries:request_commentary')) + form = RequestPublishedArticleForm() + context = { + 'form': form, + 'query_form': query_form, + } + return render(request, 'commentaries/request_published_article.html', context) + else: + raise Http404 -@method_decorator(permission_required( - 'scipost.can_request_commentary_pages', raise_exception=True), name='dispatch') -class PrefillUsingIdentifierView(RequestCommentaryMixin, FormView): - form_class = IdentifierToQueryForm - template_name = 'commentaries/request_commentary.html' - - def form_invalid(self, identifierform): - for field, errors in identifierform.errors.items(): - for error in errors: - messages.warning(self.request, error) - return render(self.request, 'commentaries/request_commentary.html', - self.get_context_data(**{})) - - def form_valid(self, identifierform): - '''Prefill using the ArxivCaller if the Identifier is valid''' - caller = ArxivCaller(Commentary, identifierform.cleaned_data['identifier']) - caller.process() - - if caller.is_valid(): - # Prefill the form - metadata = caller.metadata - pub_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 = { - 'type': 'preprint', - 'metadata': metadata, - 'pub_title': pub_title, - 'author_list': authorlist, - 'arxiv_identifier': identifierform.cleaned_data['identifier'], - 'arxiv_link': arxiv_link, - 'pub_abstract': abstract - } - context = { - 'title': pub_title, - 'request_commentary_form': RequestCommentaryForm(initial=initialdata) - } - messages.success(self.request, 'Arxiv completed') - return render(self.request, 'commentaries/request_commentary.html', - self.get_context_data(**context)) + +@permission_required('scipost.can_request_commentary_pages', raise_exception=True) +def prefill_using_arxiv_identifier(request): + if request.method == "POST": + query_form = ArxivQueryForm(request.POST) + if query_form.is_valid(): + prefill_data = query_form.request_arxiv_preprint_form_prefill_data() + form = RequestArxivPreprintForm(initial=prefill_data) + messages.success(request, strings.acknowledge_arxiv_query, fail_silently=True) else: - msg = caller.get_error_message() - messages.error(self.request, msg) - return render(self.request, 'commentaries/request_commentary.html', - self.get_context_data(**{})) + form = RequestArxivPreprintForm() + + context = { + 'form': form, + 'query_form': query_form, + } + return render(request, 'commentaries/request_arxiv_preprint.html', context) + else: + raise Http404 @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().first() # only handle one at a time - form = VetCommentaryForm() - context = {'contributor': contributor, 'commentary_to_vet': commentary_to_vet, 'form': form} + 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(): + # Get commentary + commentary = form.get_commentary() + email_context = { + 'commentary': commentary + } + + # Retrieve email_template for action + if form.commentary_is_accepted(): + email_template = 'commentaries/vet_commentary_email_accepted.html' + 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) + 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) + commentary = form.process_commentary() + + 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 vet_commentary_request_ack(request, commentary_id): - if request.method == 'POST': - form = VetCommentaryForm(request.POST, user=request.user, commentary_id=commentary_id) - if form.is_valid(): - # Get commentary - commentary = form.get_commentary() - email_context = { - 'commentary': commentary - } - - # 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'] - - # Send email and process form - email_text = render_to_string(email_template, email_context) - 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) - 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) - - 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) +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 = { + 'commentary': commentary, + 'form': form + } + return render(request, 'commentaries/modify_commentary_request.html', context) class CommentaryListView(ListView): model = Commentary form = CommentarySearchForm paginate_by = 10 + context_object_name = 'commentary_list' def get_queryset(self): '''Perform search form here already to get the right pagination numbers.''' @@ -305,7 +230,9 @@ class CommentaryListView(ListView): def commentary_detail(request, arxiv_or_DOI_string): - commentary = get_object_or_404(Commentary, arxiv_or_DOI_string=arxiv_or_DOI_string) + commentary = get_object_or_404(Commentary.objects.vetted(), + arxiv_or_DOI_string=arxiv_or_DOI_string) + comments = commentary.comment_set.all() form = CommentForm() try: diff --git a/journals/admin.py b/journals/admin.py index 79667f7bfc31d7d998b1348e5ddcfebd0645009d..48f9f4ee5a90149ca010e5fe22edf9d85e75f7c6 100644 --- a/journals/admin.py +++ b/journals/admin.py @@ -1,14 +1,8 @@ from django.contrib import admin, messages -from journals.models import ProductionStream, ProductionEvent from journals.models import Journal, Volume, Issue, Publication, Deposit -admin.site.register(ProductionStream) - - -admin.site.register(ProductionEvent) - class JournalAdmin(admin.ModelAdmin): search_fields = ['name'] diff --git a/journals/constants.py b/journals/constants.py index 3a3aa9b88e6191b3c6620a33a1d3d84fc5db0d86..6bd5de5cf9a4285d253a22a9bbb638efde5396f0 100644 --- a/journals/constants.py +++ b/journals/constants.py @@ -56,18 +56,3 @@ ISSUE_STATUSES = ( (STATUS_DRAFT, 'Draft'), (STATUS_PUBLISHED, 'Published'), ) - -PRODUCTION_STREAM_STATUS = ( - ('ongoing', 'Ongoing'), - ('completed', 'Completed'), -) - -PRODUCTION_EVENTS = ( - ('assigned_to_supervisor', 'Assigned to Supervisor'), - ('officer_tasked_with_proof_production', 'Officer tasked with proofs production'), - ('proofs_produced', 'Proofs have been produced'), - ('proofs_sent_to_authors', 'Proofs sent to Authors'), - ('proofs_returned_by_authors', 'Proofs returned by Authors'), - ('corrections_implemented', 'Corrections implemented'), - ('authors_have_accepted_proofs', 'Authors have accepted proofs'), -) diff --git a/journals/forms.py b/journals/forms.py index 910bbd5b4f375f2aa40dc63893b542de1dc68a8b..d173a344773c53a27ea61e613631c63cdf7b8edc 100644 --- a/journals/forms.py +++ b/journals/forms.py @@ -1,22 +1,11 @@ from django import forms from django.utils import timezone -from .models import ProductionEvent from .models import UnregisteredAuthor, Issue, Publication from submissions.models import Submission -class ProductionEventForm(forms.ModelForm): - class Meta: - model = ProductionEvent - exclude = ['stream', 'noted_on', 'noted_by'] - - def __init__(self, *args, **kwargs): - super(ProductionEventForm, self).__init__(*args, **kwargs) - self.fields['duration'].widget.attrs.update( - {'placeholder': 'HH:MM:SS'}) - class InitiatePublicationForm(forms.Form): accepted_submission = forms.ModelChoiceField( diff --git a/journals/migrations/0023_auto_20170517_1846.py b/journals/migrations/0023_auto_20170517_1846.py new file mode 100644 index 0000000000000000000000000000000000000000..b3462dcdd0e7b5008c780b0446f1324405f07df4 --- /dev/null +++ b/journals/migrations/0023_auto_20170517_1846.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-17 16:46 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0022_auto_20170517_1608'), + ] + + operations = [ + migrations.RemoveField( + model_name='productionevent', + name='noted_by', + ), + migrations.RemoveField( + model_name='productionevent', + name='stream', + ), + migrations.RemoveField( + model_name='productionstream', + name='submission', + ), + migrations.DeleteModel( + name='ProductionEvent', + ), + migrations.DeleteModel( + name='ProductionStream', + ), + ] diff --git a/journals/models.py b/journals/models.py index 7a413a808ed36963c70f227198e2ac79040fecf2..1d1862c0f3b71df99ca9d038738bedb9b45af0d6 100644 --- a/journals/models.py +++ b/journals/models.py @@ -7,8 +7,7 @@ from django.urls import reverse from .behaviors import doi_journal_validator, doi_volume_validator,\ doi_issue_validator, doi_publication_validator from .constants import SCIPOST_JOURNALS, SCIPOST_JOURNALS_DOMAINS,\ - STATUS_DRAFT, STATUS_PUBLISHED, ISSUE_STATUSES,\ - PRODUCTION_STREAM_STATUS, PRODUCTION_EVENTS + STATUS_DRAFT, STATUS_PUBLISHED, ISSUE_STATUSES from .helpers import paper_nr_string, journal_name_abbrev_citation from .managers import IssueManager, PublicationManager, JournalManager @@ -17,34 +16,6 @@ from scipost.fields import ChoiceArrayField from scipost.models import Contributor -############## -# Production # -############## - -class ProductionStream(models.Model): - submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE) - opened = models.DateTimeField() - - def __str__(self): - return str(self.submission) - - def total_duration(self): - totdur = self.productionevent_set.all().aggregate(models.Sum('duration')) - return totdur['duration__sum'] - - -class ProductionEvent(models.Model): - stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE) - event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS) - comments = models.TextField(blank=True, null=True) - noted_on = models.DateTimeField(default=timezone.now) - noted_by = models.ForeignKey(Contributor, on_delete=models.CASCADE) - duration = models.DurationField(blank=True, null=True) - - def __str__(self): - return '%s: %s' % (str(self.stream.submission), self.get_event_display()) - - ################ # Journals etc # ################ diff --git a/journals/templates/journals/publication_detail.html b/journals/templates/journals/publication_detail.html index 38ddbd0126458949676a818100764228b9277907..e71cba60ff028d343366224a34d76021edc3b27f 100644 --- a/journals/templates/journals/publication_detail.html +++ b/journals/templates/journals/publication_detail.html @@ -24,7 +24,7 @@ {% endfor %} <meta name="citation_doi" content="{{ publication.doi_string }}"/> <meta name="citation_publication_date" content="{{ publication.publication_date|date:'Y/m/d' }}"/> - <meta name="citation_journal_title" content="{{ journal.name }}"/> + <meta name="citation_journal_title" content="{{ journal }}"/> <meta name="citation_issn" content="{{ journal.issn }}"/> <meta name="citation_volume" content="{{ publication.in_issue.in_volume.number }}"/> <meta name="citation_issue" content="{{ publication.in_issue.number }}"/> diff --git a/journals/urls/general.py b/journals/urls/general.py index 13287579afdf1ea4e3e9361ca661578c3c82ed4c..25679dc22c730474636a0d9a468fd170ebc9973c 100644 --- a/journals/urls/general.py +++ b/journals/urls/general.py @@ -12,11 +12,6 @@ urlpatterns = [ TemplateView.as_view(template_name='journals/journals_terms_and_conditions.html'), name='journals_terms_and_conditions'), - # Production - url(r'^production$', journals_views.production, name='production'), - url(r'^add_production_event/(?P<stream_id>[0-9]+)$', - journals_views.add_production_event, name='add_production_event'), - # Editorial and Administrative Workflow url(r'^initiate_publication$', journals_views.initiate_publication, diff --git a/journals/views.py b/journals/views.py index 67301e77c8d03556df956ef7d09e1b581ed3232c..2f0f5726d1cd3f34fac17aeb46f71192dabf6fa3 100644 --- a/journals/views.py +++ b/journals/views.py @@ -15,9 +15,7 @@ from django.http import HttpResponse from .exceptions import PaperNumberingError from .helpers import paper_nr_string -from .models import ProductionStream, ProductionEvent from .models import Journal, Issue, Publication, UnregisteredAuthor -from .forms import ProductionEventForm from .forms import FundingInfoForm, InitiatePublicationForm, ValidatePublicationForm,\ UnregisteredAuthorForm, CreateMetadataXMLForm, CitationListBibitemsForm from .utils import JournalUtils @@ -142,61 +140,6 @@ def issue_detail(request, doi_label): return render(request, 'journals/journal_issue_detail.html', context) -###################### -# Production process # -###################### - -@permission_required('scipost.can_view_production', return_403=True) -def production(request): - """ - Overview page for the production process. - All papers with accepted but not yet published status are included here. - """ - accepted_submissions = Submission.objects.filter( - status='accepted').order_by('latest_activity') - streams = ProductionStream.objects.all().order_by('opened') - prodevent_form = ProductionEventForm() - context = { - 'accepted_submissions': accepted_submissions, - 'streams': streams, - 'prodevent_form': prodevent_form, - } - return render(request, 'journals/production.html', context) - -@permission_required('scipost.can_view_production', return_403=True) -@transaction.atomic -def add_production_event(request, stream_id): - stream = get_object_or_404(ProductionStream, pk=stream_id) - if request.method == 'POST': - prodevent_form = ProductionEventForm(request.POST) - if prodevent_form.is_valid(): - prodevent = ProductionEvent( - stream=stream, - event=prodevent_form.cleaned_data['event'], - comments=prodevent_form.cleaned_data['comments'], - noted_on=timezone.now(), - noted_by=request.user.contributor, - duration=prodevent_form.cleaned_data['duration'],) - prodevent.save() - return redirect(reverse('journals:production')) - else: - errormessage = 'The form was invalidly filled.' - return render(request, 'scipost/error.html', {'errormessage': errormessage}) - else: - errormessage = 'This view can only be posted to.' - return render(request, 'scipost/error.html', {'errormessage': errormessage}) - - - - -def upload_proofs(request): - """ - TODO - Called by a member of the Production Team. - Upload the production version .pdf of a submission. - """ - return render(request, 'journals/upload_proofs.html') - ####################### # Publication process # diff --git a/partners/__init__.py b/partners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/partners/admin.py b/partners/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..2b1ddd057b5450a624f52bfba070d5de1bc36f56 --- /dev/null +++ b/partners/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from .models import ContactPerson, Partner, Consortium,\ + ProspectivePartner, MembershipAgreement + + +admin.site.register(ContactPerson) + + +class PartnerAdmin(admin.ModelAdmin): + search_fields = ['institution', 'institution_acronym', + 'institution_address', 'contact_person'] + + +admin.site.register(Partner, PartnerAdmin) +admin.site.register(Consortium) +admin.site.register(ProspectivePartner) +admin.site.register(MembershipAgreement) diff --git a/partners/apps.py b/partners/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..22e6fe3bc79c57abf3a8a51ccf4f6dc9ca1ff251 --- /dev/null +++ b/partners/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PartnersConfig(AppConfig): + name = 'partners' diff --git a/partners/constants.py b/partners/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..ec05c2315596b05013b825e5c27531e75aaca543 --- /dev/null +++ b/partners/constants.py @@ -0,0 +1,43 @@ +import datetime + + +PARTNER_TYPES = ( + ('Int. Fund. Agency', 'International Funding Agency'), + ('Nat. Fund. Agency', 'National Funding Agency'), + ('Nat. Library', 'National Library'), + ('Univ. Library', 'University Library'), + ('Res. Library', 'Research Library'), + ('Foundation', 'Foundation'), + ('Individual', 'Individual'), +) + +PARTNER_STATUS = ( + ('Prospective', 'Prospective'), + ('Negotiating', 'Negotiating'), + ('Active', 'Active'), + ('Inactive', 'Inactive'), +) + + +CONSORTIUM_STATUS = ( + ('Prospective', 'Prospective'), + ('Active', 'Active'), + ('Inactive', 'Inactive'), +) + + +MEMBERSHIP_AGREEMENT_STATUS = ( + ('Submitted', 'Request submitted by Partner'), + ('Pending', 'Sent to Partner, response pending'), + ('Signed', 'Signed by Partner'), + ('Honoured', 'Honoured: payment of Partner received'), + ('Completed', 'Completed: agreement has been fulfilled'), +) + +MEMBERSHIP_DURATION = ( + (datetime.timedelta(days=365), '1 year'), + (datetime.timedelta(days=730), '2 years'), + (datetime.timedelta(days=1095), '3 years'), + (datetime.timedelta(days=1460), '4 years'), + (datetime.timedelta(days=1825), '5 years'), +) diff --git a/partners/forms.py b/partners/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..3aa5ed7fe1168577f6b676b20755236600e105d2 --- /dev/null +++ b/partners/forms.py @@ -0,0 +1,47 @@ +from django import forms + +from captcha.fields import ReCaptchaField +from django_countries import countries +from django_countries.widgets import CountrySelectWidget +from django_countries.fields import LazyTypedChoiceField + +from .constants import PARTNER_TYPES +from .models import ContactPerson, Partner, ProspectivePartner, MembershipAgreement + +from scipost.models import TITLE_CHOICES + + +class PartnerForm(forms.ModelForm): + class Meta: + model = Partner + fields = '__all__' + + def __init__(self, *args, **kwargs): + super(PartnerForm, self).__init__(*args, **kwargs) + self.fields['institution_address'].widget = forms.Textarea({'rows': 8, }) + + +class ProspectivePartnerForm(forms.ModelForm): + class Meta: + model = ProspectivePartner + exclude = ['date_received', 'date_processed', 'processed'] + + +class MembershipQueryForm(forms.Form): + """ + This form is to be used by an agent of the prospective Partner, + in order to request more information about potentially joining the SPB. + """ + title = forms.ChoiceField(choices=TITLE_CHOICES, label='* Your title') + first_name = forms.CharField(label='* Your first name', max_length=100) + last_name = forms.CharField(label='* Your last name', max_length=100) + email = forms.EmailField(label='* Your email address') + role = forms.CharField(label='* Your role in your organization') + partner_type = forms.ChoiceField(choices=PARTNER_TYPES, label='* Partner type') + institution_name = forms.CharField(label='* Name of your institution') + country = LazyTypedChoiceField( + choices=countries, label='* Country', initial='NL', + widget=CountrySelectWidget(layout=( + '{widget}<img class="country-select-flag" id="{flag_id}"' + ' style="margin: 6px 4px 0" src="{country.flag}">'))) + captcha = ReCaptchaField(attrs={'theme': 'clean'}, label='*Please verify to continue:') diff --git a/partners/migrations/0001_initial.py b/partners/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..9f7fdd7dcfe5882c5057e8d2d8991c996d956abc --- /dev/null +++ b/partners/migrations/0001_initial.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 08:59 +from __future__ import unicode_literals + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django_countries.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Consortium', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128)), + ('status', models.CharField(choices=[('Prospective', 'Prospective'), ('Active', 'Active'), ('Inactive', 'Inactive')], max_length=16)), + ], + ), + migrations.CreateModel( + name='ContactPerson', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='MembershipAgreement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('Submitted', 'Request submitted by Partner'), ('Pending', 'Sent to Partner, response pending'), ('Signed', 'Signed by Partner'), ('Honoured', 'Honoured: payment of Partner received'), ('Completed', 'Completed: agreement has been fulfilled')], max_length=16)), + ('date_requested', models.DateField()), + ('start_date', models.DateField()), + ('duration', models.DurationField(choices=[(datetime.timedelta(365), '1 year'), (datetime.timedelta(730), '2 years'), (datetime.timedelta(1095), '3 years'), (datetime.timedelta(1460), '4 years'), (datetime.timedelta(1825), '5 years')])), + ('offered_yearly_contribution', models.SmallIntegerField(default=0)), + ('consortium', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partners.Consortium')), + ], + ), + migrations.CreateModel( + name='Partner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('status', models.CharField(choices=[('Prospective', 'Prospective'), ('Negotiating', 'Negotiating'), ('Active', 'Active'), ('Inactive', 'Inactive')], max_length=16)), + ('institution_name', models.CharField(max_length=256)), + ('institution_acronym', models.CharField(max_length=10)), + ('institution_address', models.CharField(blank=True, max_length=1000, null=True)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('financial_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_financial_contact', to='partners.ContactPerson')), + ('main_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_main_contact', to='partners.ContactPerson')), + ('technical_contact', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partner_technical_contact', to='partners.ContactPerson')), + ], + ), + migrations.AddField( + model_name='membershipagreement', + name='partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='partners.Partner'), + ), + migrations.AddField( + model_name='consortium', + name='partners', + field=models.ManyToManyField(blank=True, to='partners.Partner'), + ), + ] diff --git a/partners/migrations/0002_auto_20170519_1335.py b/partners/migrations/0002_auto_20170519_1335.py new file mode 100644 index 0000000000000000000000000000000000000000..8bffe6c9bbe4a80f18c7533550ad3b1ebce65321 --- /dev/null +++ b/partners/migrations/0002_auto_20170519_1335.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 11:35 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MembershipQuery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('first_name', models.CharField(max_length=32)), + ('last_name', models.CharField(max_length=32)), + ('email', models.EmailField(max_length=254)), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('institution_name', models.CharField(max_length=256)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('date_received', models.DateTimeField(default=django.utils.timezone.now)), + ('date_processed', models.DateTimeField()), + ('processed', models.BooleanField(default=False)), + ], + options={ + 'verbose_name_plural': 'membership queries', + }, + ), + migrations.AlterModelOptions( + name='consortium', + options={'verbose_name_plural': 'consortia'}, + ), + ] diff --git a/partners/migrations/0003_auto_20170519_1424.py b/partners/migrations/0003_auto_20170519_1424.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d9c35d8fcd8f7dc056c421fe2b0b32ceae6339 --- /dev/null +++ b/partners/migrations/0003_auto_20170519_1424.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 12:24 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone +import django_countries.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0002_auto_20170519_1335'), + ] + + operations = [ + migrations.CreateModel( + name='ProspectivePartner', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(choices=[('PR', 'Prof.'), ('DR', 'Dr'), ('MR', 'Mr'), ('MRS', 'Mrs')], max_length=4)), + ('first_name', models.CharField(max_length=32)), + ('last_name', models.CharField(max_length=32)), + ('email', models.EmailField(max_length=254)), + ('role', models.CharField(max_length=128)), + ('partner_type', models.CharField(choices=[('Int. Fund. Agency', 'International Funding Agency'), ('Nat. Fund. Agency', 'National Funding Agency'), ('Nat. Library', 'National Library'), ('Univ. Library', 'University Library'), ('Res. Library', 'Research Library'), ('Foundation', 'Foundation'), ('Individual', 'Individual')], max_length=32)), + ('institution_name', models.CharField(max_length=256)), + ('country', django_countries.fields.CountryField(max_length=2)), + ('date_received', models.DateTimeField(default=django.utils.timezone.now)), + ('date_processed', models.DateTimeField()), + ('processed', models.BooleanField(default=False)), + ], + ), + migrations.DeleteModel( + name='MembershipQuery', + ), + ] diff --git a/partners/migrations/0004_auto_20170519_1425.py b/partners/migrations/0004_auto_20170519_1425.py new file mode 100644 index 0000000000000000000000000000000000000000..f44c307494246de26b93ad9a8dc869f5a9d3a8a2 --- /dev/null +++ b/partners/migrations/0004_auto_20170519_1425.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 12:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0003_auto_20170519_1424'), + ] + + operations = [ + migrations.AlterField( + model_name='prospectivepartner', + name='date_processed', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/partners/migrations/__init__.py b/partners/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/partners/models.py b/partners/models.py new file mode 100644 index 0000000000000000000000000000000000000000..82450ab9946f58536bb1892d64b2d97b3a296401 --- /dev/null +++ b/partners/models.py @@ -0,0 +1,105 @@ +from django.contrib.auth.models import User +from django.db import models +from django.utils import timezone + +from django_countries.fields import CountryField + +from .constants import PARTNER_TYPES, PARTNER_STATUS, CONSORTIUM_STATUS,\ + MEMBERSHIP_AGREEMENT_STATUS, MEMBERSHIP_DURATION + +from scipost.constants import TITLE_CHOICES + + +class ContactPerson(models.Model): + """ + A ContactPerson is a simple form of User which is meant + to be associated to Partner objects + (main contact, financial/technical contact etc). + ContactPersons and Contributors have different rights. + """ + user = models.OneToOneField(User, on_delete=models.CASCADE, unique=True) + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + + def __str__(self): + return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name) + + +class Partner(models.Model): + """ + Supporting Partners. + These are the official Partner objects created by SciPost Admin. + """ + partner_type = models.CharField(max_length=32, choices=PARTNER_TYPES) + status = models.CharField(max_length=16, choices=PARTNER_STATUS) + institution_name = models.CharField(max_length=256) + institution_acronym = models.CharField(max_length=10) + institution_address = models.CharField(max_length=1000, blank=True, null=True) + country = CountryField() + main_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_main_contact') + financial_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_financial_contact') + technical_contact = models.ForeignKey(ContactPerson, on_delete=models.CASCADE, + blank=True, null=True, + related_name='partner_technical_contact') + + def __str__(self): + return self.institution_acronym + ' (' + self.get_status_display() + ')' + + +class Consortium(models.Model): + """ + Collection of Partners. + """ + name = models.CharField(max_length=128) + partners = models.ManyToManyField(Partner, blank=True) + status = models.CharField(max_length=16, choices=CONSORTIUM_STATUS) + + class Meta: + verbose_name_plural = 'consortia' + + +class ProspectivePartner(models.Model): + """ + Created from the membership_request page, after submitting a query form. + """ + title = models.CharField(max_length=4, choices=TITLE_CHOICES) + first_name = models.CharField(max_length=32) + last_name = models.CharField(max_length=32) + email = models.EmailField() + role = models.CharField(max_length=128) + partner_type = models.CharField(max_length=32, choices=PARTNER_TYPES) + institution_name = models.CharField(max_length=256) + country = CountryField() + date_received = models.DateTimeField(default=timezone.now) + date_processed = models.DateTimeField(blank=True, null=True) + processed = models.BooleanField(default=False) + + def __str__(self): + resp = "processed" + if not self.processed: + resp = "unprocessed" + return '%s (received %s), %s' % (self.institution_name, + self.date_received.strftime("%Y-%m-%d"), + resp) + + +class MembershipAgreement(models.Model): + """ + Agreement for membership of the Supporting Partners Board. + A new instance is created each time an Agreement is made or renewed. + """ + partner = models.ForeignKey(Partner, on_delete=models.CASCADE, blank=True, null=True) + consortium = models.ForeignKey(Consortium, on_delete=models.CASCADE, blank=True, null=True) + status = models.CharField(max_length=16, choices=MEMBERSHIP_AGREEMENT_STATUS) + date_requested = models.DateField() + start_date = models.DateField() + duration = models.DurationField(choices=MEMBERSHIP_DURATION) + offered_yearly_contribution = models.SmallIntegerField(default=0) + + def __str__(self): + return (str(self.partner) + + ' [' + self.get_duration_display() + + ' from ' + self.start_date.strftime('%Y-%m-%d') + ']') diff --git a/partners/templates/partners/_partner_card.html b/partners/templates/partners/_partner_card.html new file mode 100644 index 0000000000000000000000000000000000000000..31175dccfad720d674489d66f3165abe4803c9af --- /dev/null +++ b/partners/templates/partners/_partner_card.html @@ -0,0 +1,31 @@ +{% load bootstrap %} + +<div class="card-block"> + <div class="row"> + <div class="col-1"> + <p>{{ partner.country }}</p> + </div> + <div class="col-4"> + <h3>{{ partner.institution_name }}</h3> + <p>{{ partner.institution_acronym }}</p> + <p>({{ pp.get_partner_type_display }})</p> + </div> + <div class="col-4"> + {% if partner.main_contact %} + <p>Main contact: {{ partner.main_contact..get_title_display }} {{ partner.main_contact.user.first_name }} {{ partner.main_contact.user.last_name }}</p> + <p>{{ partner.main_contact.user.email }}</p> + {% endif %} + {% if partner.financial_contact %} + <p>Financial contact: {{ partner.financial_contact..get_title_display }} {{ partner.financial_contact.user.first_name }} {{ partner.financial_contact.user.last_name }}</p> + <p>{{ partner.financial_contact.user.email }}</p> + {% endif %} + {% if partner.technical_contact %} + <p>Technical contact: {{ partner.technical_contact..get_title_display }} {{ partner.technical_contact.user.first_name }} {{ partner.technical_contact.user.last_name }}</p> + <p>{{ partner.technical_contact.user.email }}</p> + {% endif %} + </div> + <div class="col-3"> + <p>Edit</p> + </div> + </div> +</div> diff --git a/partners/templates/partners/_prospective_partner_card.html b/partners/templates/partners/_prospective_partner_card.html new file mode 100644 index 0000000000000000000000000000000000000000..fe2624c247b05a878622743b0c213a80467220cc --- /dev/null +++ b/partners/templates/partners/_prospective_partner_card.html @@ -0,0 +1,22 @@ +{% load bootstrap %} + +<div class="card-block"> + <div class="row"> + <div class="col-1"> + <p>{{ pp.country }}</p> + </div> + <div class="col-4"> + <h3>{{ pp.institution_name }}</h3> + <p>({{ pp.get_partner_type_display }})</p> + <p>Received {{ pp.date_received }}</p> + </div> + <div class="col-4"> + <p>Contact: {{ pp.get_title_display }} {{ pp.first_name }} {{ pp.last_name }}</p> + <p>(role: {{ pp.role }})</p> + <p>{{ pp.email }}</p> + </div> + <div class="col-3"> + <p>Edit</p> + </div> + </div> +</div> diff --git a/partners/templates/partners/add_prospective_partner.html b/partners/templates/partners/add_prospective_partner.html new file mode 100644 index 0000000000000000000000000000000000000000..737780443f5c8da12d23699d2845e666fbf1ce14 --- /dev/null +++ b/partners/templates/partners/add_prospective_partner.html @@ -0,0 +1,29 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners: add{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h1>Add a Prospective Partner</h1> + </div> + </div> + <p>Please provide contact details of an appropriate representative, and details about the potential Partner.</p> + + <form action="{% url 'partners:add_prospective_partner' %}" method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> + </form> + + {% if errormessage %} + <p class="text-danger">{{ errormessage }}</p> + {% endif %} + +</section> + +{% endblock content %} diff --git a/partners/templates/partners/manage_partners.html b/partners/templates/partners/manage_partners.html new file mode 100644 index 0000000000000000000000000000000000000000..95fb172639561b9c03f3d03c7a97711b0ccdf18a --- /dev/null +++ b/partners/templates/partners/manage_partners.html @@ -0,0 +1,55 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: Supporting Partners: manage{% endblock pagetitle %} + + +{% block content %} + +<div class="flex-container"> + <div class="flex-greybox"> + <h1>Partners Management Page</h1> + </div> +</div> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Partners</h2> + </div> + </div> + <ul class="list-group list-group-flush"> + {% for partner in partners %} + <li class="list-group-item">{% include 'partners/_partner_card.html' with partner=partner %}</li> + {% endfor %} + </ul> +</section> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Prospective Partners (not yet processed)</h2> + </div> + </div> + <h3><a href="{% url 'partners:add_prospective_partner' %}">Add a prospective partner</a></h3> + <br/> + <ul class="list-group list-group-flush"> + {% for partner in prospective_partners %} + <li class="list-group-item">{% include 'partners/_prospective_partner_card.html' with pp=partner %}</li> + {% endfor %} + </ul> +</section> + +<section> + <div class="flex-container"> + <div class="flex-greybox"> + <h2>Agreements</h2> + </div> + </div> + <ul> + {% for agreement in agreements %} + <li>{{ agreement }}</li> + {% endfor %} + </ul> +</section> + +{% endblock content %} diff --git a/scipost/templates/scipost/SPB_membership_request.html b/partners/templates/partners/membership_request.html similarity index 74% rename from scipost/templates/scipost/SPB_membership_request.html rename to partners/templates/partners/membership_request.html index 2048761d33e9e9e89f7d71d9ba4181d756e24c01..13e312907df28f848d1eeb15705c45a45ed33cb9 100644 --- a/scipost/templates/scipost/SPB_membership_request.html +++ b/partners/templates/partners/membership_request.html @@ -37,27 +37,27 @@ $(document).ready(function(){ <div class="flex-container"> <div class="flex-whitebox"> - <p>You can hereby initiate the process to become one of our Supporting Partners.</p> + <p>You can hereby request further details on the process to become one + of our Supporting Partners.</p> <p>Filling in this form does not yet constitute a binding agreement.</p> <p>It simply expresses your interest in considering joining our Supporting Partners Board.</p> - <p>After filling this form, SciPost Administration will contact you with a Partnership - Agreement offer.</p> + <p>After filling this form, SciPost Administration will contact you with more details on Partnership.</p> <p><em>Note: you will automatically be considered as the contact person for this Partner.</em></p> {% if errormessage %} <p style="color: red;">{{ errormessage }}</p> {% endif %} - <form action="{% url 'scipost:SPB_membership_request' %}" method="post"> + <form action="{% url 'partners:membership_request' %}" method="post"> {% csrf_token %} - <h3>Partner details:</h3> - - {{ SP_form|bootstrap }} - <h3>Agreement terms:</h3> - {{ membership_form|bootstrap }} - <input class="btn btn-secondary" type="submit" value="Submit"/> + <h3>Please provide us the following relevant details:</h3> + {{ query_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Submit"/> </form> + {% if errormessage %} + <p class="text-danger">{{ errormessage }}</p> + {% endif %} </div> </div> diff --git a/scipost/templates/scipost/supporting_partners.html b/partners/templates/partners/supporting_partners.html similarity index 93% rename from scipost/templates/scipost/supporting_partners.html rename to partners/templates/partners/supporting_partners.html index f5ced193e5cf7364cf6501cbbdf892a32e365e58..b1434fb8c75e7465993224ad3118362e8ac9b45b 100644 --- a/scipost/templates/scipost/supporting_partners.html +++ b/partners/templates/partners/supporting_partners.html @@ -8,7 +8,6 @@ {% block bodysup %} - <section> <div class="flex-container"> <div class="flex-greybox"> @@ -16,6 +15,10 @@ </div> </div> + {% if perms.scipost.can_manage_SPB %} + <a href="{% url 'partners:manage' %}">Manage Partners</a> + {% endif %} + <div class="flex-container"> <div class="flex-whitebox"> @@ -41,7 +44,7 @@ <p>We hereby cordially invite interested parties who are supportive of SciPost's mission to join the SciPost Supporting Partners Board by signing a <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a>.</p> - <p>Prospective partners can initiate the process leading to Membership by filling the <a href="{% url 'scipost:SPB_membership_request' %}">online request form</a>.</p> + <p>Prospective partners can query for more information about Membership by filling the <a href="{% url 'partners:membership_request' %}">online query form</a>.</p> <br/> <p>The <a href="{% static 'scipost/SPB/SciPost_Supporting_Partner_Agreement.pdf' %}">Partner Agreement</a> itself contains a detailed presentation of the Foundation, its activities and financial aspects. What follows is a summary of the most important points.</p> @@ -107,7 +110,7 @@ <h3>Activation procedure</h3> <p>In order to become a Supporting Partner, one must: <ul> - <li>Fill in the online <a href="{% url 'scipost:SPB_membership_request' %}">membership request form</a> (the form must be filled in by a registered Contributor, employed by or associated to the prospective Partner and acting as an authorized agent for the latter; personal contact details of this person will be treated confidentially).</li> + <li>Fill in the online <a href="{% url 'partners:membership_request' %}">membership request form</a> (the form must be filled in by an authorized agent employed by or associated to the prospective Partner; personal contact details of this person will be treated confidentially).</li> <li>Wait for the email response from the SciPost administration, containing a Partnership Agreement offer including detailed terms (start date, duration, financial contribution).</li> <li>Email a scan of the signed copy of the Partnership Agreement to SciPost.</li> <li>Proceed with the payment of the financial contribution, following invoicing from the SciPost Foundation.</li> diff --git a/partners/tests.py b/partners/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/partners/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/partners/urls.py b/partners/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..5558aaaed1659597f7511d16fc50b80acc947868 --- /dev/null +++ b/partners/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import url + +from . import views + +urlpatterns = [ + + url(r'^$', views.supporting_partners, + name='partners'), + url(r'^membership_request$', views.membership_request, + name='membership_request'), + url(r'^manage$', views.manage, name='manage'), + url(r'^add_prospective_partner$', views.add_prospective_partner, + name='add_prospective_partner'), +] diff --git a/partners/views.py b/partners/views.py new file mode 100644 index 0000000000000000000000000000000000000000..7473a5352a699f7b2c940ad97dcffaab3554fd68 --- /dev/null +++ b/partners/views.py @@ -0,0 +1,64 @@ +from django.contrib import messages +from django.shortcuts import render, reverse, redirect +from django.utils import timezone + +from guardian.decorators import permission_required + +from .models import Partner, ProspectivePartner, MembershipAgreement +from .forms import ProspectivePartnerForm, MembershipQueryForm + + +def supporting_partners(request): + prospective_agreements = MembershipAgreement.objects.filter( + status='Submitted').order_by('date_requested') + context = {'prospective_partners': prospective_agreements, } + return render(request, 'partners/supporting_partners.html', context) + + +def membership_request(request): + query_form = MembershipQueryForm(request.POST or None) + if query_form.is_valid(): + query = ProspectivePartner( + title=query_form.cleaned_data['title'], + first_name=query_form.cleaned_data['first_name'], + last_name=query_form.cleaned_data['last_name'], + email=query_form.cleaned_data['email'], + partner_type=query_form.cleaned_data['partner_type'], + institution_name=query_form.cleaned_data['institution_hame'], + country=query_form.cleaned_data['country'], + date_received=timezone.now(), + ) + query.save() + ack_message = ('Thank you for your SPB Membership query. ' + 'We will get back to you in the very near future ' + 'with further details.') + context = {'ack_message': ack_message, } + return render(request, 'scipost/acknowledgement.html', context) + context = {'query_form': query_form} + return render(request, 'partners/membership_request.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def manage(request): + """ + Lists relevant info regarding management of Supporting Partners Board. + """ + partners = Partner.objects.all().order_by('country', 'institution_name') + prospective_partners = ProspectivePartner.objects.filter( + processed=False).order_by('date_received') + agreements = MembershipAgreement.objects.all().order_by('date_requested') + context = {'partners': partners, + 'prospective_partners': prospective_partners, + 'agreements': agreements, } + return render(request, 'partners/manage_partners.html', context) + + +@permission_required('scipost.can_manage_SPB', return_403=True) +def add_prospective_partner(request): + form = ProspectivePartnerForm(request.POST or None) + if form.is_valid(): + form.save() + messages.success(request, 'Prospective Partners successfully added') + return redirect(reverse('partners:manage')) + context = {'form': form} + return render(request, 'partners/add_prospective_partner.html', context) diff --git a/production/__init__.py b/production/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/production/admin.py b/production/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..41ae12e06e49ec6a43fc034ed520b5708e3b2ff0 --- /dev/null +++ b/production/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +from .models import ProductionStream, ProductionEvent + + +admin.site.register(ProductionStream) +admin.site.register(ProductionEvent) diff --git a/production/apps.py b/production/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..d8fba60df00793d7fbaff3cf230a0ba937c9576a --- /dev/null +++ b/production/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductionConfig(AppConfig): + name = 'production' diff --git a/production/constants.py b/production/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..63d9f65fef9e6df30ea6af26cb5767a5fa46bec8 --- /dev/null +++ b/production/constants.py @@ -0,0 +1,19 @@ +PRODUCTION_STREAM_STATUS = ( + ('ongoing', 'Ongoing'), + ('completed', 'Completed'), +) + + +PRODUCTION_EVENTS = ( + ('assigned_to_supervisor', 'Assigned by EdAdmin to Supervisor'), + ('message_edadmin_to_supervisor', 'Message from EdAdmin to Supervisor'), + ('message_supervisor_to_edadmin', 'Message from Supervisor to EdAdmin'), + ('officer_tasked_with_proof_production', 'Supervisor tasked officer with proofs production'), + ('message_supervisor_to_officer', 'Message from Supervisor to Officer'), + ('message_officer_to_supervisor', 'Message from Officer to Supervisor'), + ('proofs_produced', 'Proofs have been produced'), + ('proofs_sent_to_authors', 'Proofs sent to Authors'), + ('proofs_returned_by_authors', 'Proofs returned by Authors'), + ('corrections_implemented', 'Corrections implemented'), + ('authors_have_accepted_proofs', 'Authors have accepted proofs'), +) diff --git a/production/forms.py b/production/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..9f47ebe6755bf04607cf9f15f4515f73d7d090f0 --- /dev/null +++ b/production/forms.py @@ -0,0 +1,13 @@ +from django import forms + +from .models import ProductionEvent + + +class ProductionEventForm(forms.ModelForm): + class Meta: + model = ProductionEvent + exclude = ['stream', 'noted_on', 'noted_by'] + widgets = { + 'comments': forms.Textarea(attrs={'rows': 4}), + 'duration': forms.TextInput(attrs={'placeholder': 'HH:MM:SS'}) + } diff --git a/production/migrations/0001_initial.py b/production/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3ed1ac3f9aff3d6ab3a6ab1f0367aea832859d --- /dev/null +++ b/production/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-17 17:23 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('submissions', '0043_auto_20170512_0836'), + ('scipost', '0054_delete_newsitem'), + ] + + operations = [ + migrations.CreateModel( + name='ProductionEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('event', models.CharField(choices=[('assigned_to_supervisor', 'Assigned to Supervisor'), ('officer_tasked_with_proof_production', 'Officer tasked with proofs production'), ('proofs_produced', 'Proofs have been produced'), ('proofs_sent_to_authors', 'Proofs sent to Authors'), ('proofs_returned_by_authors', 'Proofs returned by Authors'), ('corrections_implemented', 'Corrections implemented'), ('authors_have_accepted_proofs', 'Authors have accepted proofs')], max_length=64)), + ('comments', models.TextField(blank=True, null=True)), + ('noted_on', models.DateTimeField(default=django.utils.timezone.now)), + ('duration', models.DurationField(blank=True, null=True)), + ('noted_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')), + ], + ), + migrations.CreateModel( + name='ProductionStream', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('opened', models.DateTimeField()), + ('submission', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='submissions.Submission')), + ], + ), + migrations.AddField( + model_name='productionevent', + name='stream', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='production.ProductionStream'), + ), + ] diff --git a/production/migrations/0002_auto_20170517_1942.py b/production/migrations/0002_auto_20170517_1942.py new file mode 100644 index 0000000000000000000000000000000000000000..53aa4d79751c03ad4491b5da6731abc11d705509 --- /dev/null +++ b/production/migrations/0002_auto_20170517_1942.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-17 17:42 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='productionevent', + name='event', + field=models.CharField(choices=[('assigned_to_supervisor', 'Assigned by EdAdmin to Supervisor'), ('message_edadmin_to_supervisor', 'Message from EdAdmin to Supervisor'), ('message_supervisor_to_edadmin', 'Message from Supervisor to EdAdmin'), ('officer_tasked_with_proof_production', 'Supervisor tasked officer with proofs production'), ('message_supervisor_to_officer', 'Message from Supervisor to Officer'), ('message_officer_to_supervisor', 'Message from Officer to Supervisor'), ('proofs_produced', 'Proofs have been produced'), ('proofs_sent_to_authors', 'Proofs sent to Authors'), ('proofs_returned_by_authors', 'Proofs returned by Authors'), ('corrections_implemented', 'Corrections implemented'), ('authors_have_accepted_proofs', 'Authors have accepted proofs')], max_length=64), + ), + ] diff --git a/production/migrations/__init__.py b/production/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/production/models.py b/production/models.py new file mode 100644 index 0000000000000000000000000000000000000000..c2cb30209aa2153f2c6b3e2365ce3e1dd6fd2b95 --- /dev/null +++ b/production/models.py @@ -0,0 +1,34 @@ +from django.db import models +from django.utils import timezone + +from .constants import PRODUCTION_EVENTS + +from scipost.models import Contributor + + +############## +# Production # +############## + +class ProductionStream(models.Model): + submission = models.OneToOneField('submissions.Submission', on_delete=models.CASCADE) + opened = models.DateTimeField() + + def __str__(self): + return str(self.submission) + + def total_duration(self): + totdur = self.productionevent_set.all().aggregate(models.Sum('duration')) + return totdur['duration__sum'] + + +class ProductionEvent(models.Model): + stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE) + event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS) + comments = models.TextField(blank=True, null=True) + noted_on = models.DateTimeField(default=timezone.now) + noted_by = models.ForeignKey(Contributor, on_delete=models.CASCADE) + duration = models.DurationField(blank=True, null=True) + + def __str__(self): + return '%s: %s' % (str(self.stream.submission), self.get_event_display()) diff --git a/journals/templates/journals/_production_event_li.html b/production/templates/production/_production_event_li.html similarity index 100% rename from journals/templates/journals/_production_event_li.html rename to production/templates/production/_production_event_li.html diff --git a/journals/templates/journals/_production_stream_card.html b/production/templates/production/_production_stream_card.html similarity index 81% rename from journals/templates/journals/_production_stream_card.html rename to production/templates/production/_production_stream_card.html index f2a78b13fd8770a13b8064ac0bbbbe7557add4b8..fda086794d4c0faa5efcdf4bbc80d314b7dc2a26 100644 --- a/journals/templates/journals/_production_stream_card.html +++ b/production/templates/production/_production_stream_card.html @@ -8,7 +8,7 @@ <h3>Events</h3> <ul> {% for event in stream.productionevent_set.all %} - {% include 'journals/_production_event_li.html' with event=event %} + {% include 'production/_production_event_li.html' with event=event %} {% empty %} <li>No events were found.</li> {% endfor %} @@ -20,7 +20,7 @@ </div> <div class="col-5"> <h3>Add an event to this production stream:</h3> - <form action="{% url 'journals:add_production_event' stream_id=stream.id %}" method="post"> + <form action="{% url 'production:add_event' stream_id=stream.id %}" method="post"> {% csrf_token %} {{ form|bootstrap }} <input type="submit" name="submit" value="Submit"> diff --git a/journals/templates/journals/production.html b/production/templates/production/production.html similarity index 79% rename from journals/templates/journals/production.html rename to production/templates/production/production.html index 2f1b67f3a957bf520a09d7a461936822d45a3161..36b7dcf39eccbc0c9982f5ebe5703962d4955682 100644 --- a/journals/templates/journals/production.html +++ b/production/templates/production/production.html @@ -10,7 +10,7 @@ <ul class="list-group list-group-flush"> {% for stream in streams %} <li class="list-group-item"> - {% include 'journals/_production_stream_card.html' with stream=stream form=prodevent_form %} + {% include 'production/_production_stream_card.html' with stream=stream form=prodevent_form %} </li> <hr/> {% endfor %} diff --git a/production/tests.py b/production/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/production/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/production/urls.py b/production/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..11007074f3a47e420c14b81f62009486dab8f23c --- /dev/null +++ b/production/urls.py @@ -0,0 +1,9 @@ +from django.conf.urls import url + +from production import views as production_views + +urlpatterns = [ + url(r'^$', production_views.production, name='production'), + url(r'^add_event/(?P<stream_id>[0-9]+)$', + production_views.add_event, name='add_event'), +] diff --git a/production/views.py b/production/views.py new file mode 100644 index 0000000000000000000000000000000000000000..4093d7751d30398ce87f78f36b3482b1b5dae76f --- /dev/null +++ b/production/views.py @@ -0,0 +1,66 @@ +from django.core.urlresolvers import reverse +from django.db import transaction +from django.shortcuts import get_object_or_404, render, redirect +from django.utils import timezone + +from guardian.decorators import permission_required + +from .models import ProductionStream, ProductionEvent +from .forms import ProductionEventForm + +from submissions.models import Submission + + +###################### +# Production process # +###################### + +@permission_required('scipost.can_view_production', return_403=True) +def production(request): + """ + Overview page for the production process. + All papers with accepted but not yet published status are included here. + """ + accepted_submissions = Submission.objects.filter( + status='accepted').order_by('latest_activity') + streams = ProductionStream.objects.all().order_by('opened') + prodevent_form = ProductionEventForm() + context = { + 'accepted_submissions': accepted_submissions, + 'streams': streams, + 'prodevent_form': prodevent_form, + } + return render(request, 'production/production.html', context) + + +@permission_required('scipost.can_view_production', return_403=True) +@transaction.atomic +def add_event(request, stream_id): + stream = get_object_or_404(ProductionStream, pk=stream_id) + if request.method == 'POST': + prodevent_form = ProductionEventForm(request.POST) + if prodevent_form.is_valid(): + prodevent = ProductionEvent( + stream=stream, + event=prodevent_form.cleaned_data['event'], + comments=prodevent_form.cleaned_data['comments'], + noted_on=timezone.now(), + noted_by=request.user.contributor, + duration=prodevent_form.cleaned_data['duration'],) + prodevent.save() + return redirect(reverse('production:production')) + else: + errormessage = 'The form was invalidly filled.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + else: + errormessage = 'This view can only be posted to.' + return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +def upload_proofs(request): + """ + TODO + Called by a member of the Production Team. + Upload the production version .pdf of a submission. + """ + return render(request, 'production/upload_proofs.html') diff --git a/scipost/admin.py b/scipost/admin.py index af7ea2f7840bc90c5f207a131569457c6694055d..2f137a42295438e82e056a70de4a15d600104ca4 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -8,7 +8,7 @@ from django.contrib.auth.models import User, Permission from scipost.models import Contributor, Remark,\ DraftInvitation,\ AffiliationObject,\ - SupportingPartner, SPBMembershipAgreement, RegistrationInvitation,\ + RegistrationInvitation,\ AuthorshipClaim, PrecookedEmail,\ EditorialCollege, EditorialCollegeFellowship @@ -126,20 +126,6 @@ class AffiliationObjectAdmin(admin.ModelAdmin): admin.site.register(AffiliationObject, AffiliationObjectAdmin) -class SPBMembershipAgreementInline(admin.StackedInline): - model = SPBMembershipAgreement - - -class SupportingPartnerAdmin(admin.ModelAdmin): - search_fields = ['institution', 'institution_acronym', - 'institution_address', 'contact_person'] - inlines = [ - SPBMembershipAgreementInline, - ] - - -admin.site.register(SupportingPartner, SupportingPartnerAdmin) - class EditorialCollegeAdmin(admin.ModelAdmin): search_fields = ['discipline', 'member'] diff --git a/scipost/constants.py b/scipost/constants.py index 790f06d6669469d03a4d664a8a4d0a865a6d0095..aa19e9b476d151d4400e2a14037b60c24b4a17b9 100644 --- a/scipost/constants.py +++ b/scipost/constants.py @@ -1,4 +1,3 @@ -import datetime DISCIPLINE_PHYSICS = 'physics' @@ -186,39 +185,3 @@ SCIPOST_FROM_ADDRESSES = ( ('J. van Wezel', 'J. van Wezel <vanwezel@scipost.org>'), ) SciPost_from_addresses_dict = dict(SCIPOST_FROM_ADDRESSES) - -# -# Supporting partner models -# -PARTNER_TYPES = ( - ('Int. Fund. Agency', 'International Funding Agency'), - ('Nat. Fund. Agency', 'National Funding Agency'), - ('Nat. Library', 'National Library'), - ('Univ. Library', 'University Library'), - ('Res. Library', 'Research Library'), - ('Consortium', 'Consortium'), - ('Foundation', 'Foundation'), - ('Individual', 'Individual'), -) - -PARTNER_STATUS = ( - ('Prospective', 'Prospective'), - ('Active', 'Active'), - ('Inactive', 'Inactive'), -) - - -SPB_MEMBERSHIP_AGREEMENT_STATUS = ( - ('Submitted', 'Request submitted by Partner'), - ('Pending', 'Sent to Partner, response pending'), - ('Signed', 'Signed by Partner'), - ('Honoured', 'Honoured: payment of Partner received'), -) - -SPB_MEMBERSHIP_DURATION = ( - (datetime.timedelta(days=365), '1 year'), - (datetime.timedelta(days=730), '2 years'), - (datetime.timedelta(days=1095), '3 years'), - (datetime.timedelta(days=1460), '4 years'), - (datetime.timedelta(days=1825), '5 years'), -) diff --git a/scipost/factories.py b/scipost/factories.py index ce122a6556cc9fb8ffdc1666608f2197382a2b7a..67cb4e2df31821db68e780bbf44fef72d83636c2 100644 --- a/scipost/factories.py +++ b/scipost/factories.py @@ -22,6 +22,8 @@ class ContributorFactory(factory.django.DjangoModelFactory): country_of_employment = factory.Iterator(list(COUNTRIES)) affiliation = factory.Faker('company') expertises = factory.Iterator(SCIPOST_SUBJECT_AREAS[0][1], getter=lambda c: [c[0]]) + personalwebpage = factory.Faker('domain_name') + address = factory.Faker('address') class Meta: model = Contributor diff --git a/scipost/forms.py b/scipost/forms.py index e1508ded5f231432cd2cb86afe3ff354c3b84cd1..7d830980a558b50c03ae13a09758c4875e4a4852 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -16,7 +16,6 @@ from crispy_forms.layout import Layout, Div, Field, HTML from .constants import SCIPOST_DISCIPLINES, TITLE_CHOICES, SCIPOST_FROM_ADDRESSES from .models import Contributor, DraftInvitation, RegistrationInvitation,\ - SupportingPartner, SPBMembershipAgreement,\ UnavailabilityPeriod, PrecookedEmail from journals.models import Publication @@ -311,62 +310,3 @@ class SendPrecookedEmailForm(forms.Form): required=False, initial=False, label='Include SciPost summary at end of message') from_address = forms.ChoiceField(choices=SCIPOST_FROM_ADDRESSES) - - -############################# -# Supporting Partners Board # -############################# - -class SupportingPartnerForm(forms.ModelForm): - class Meta: - model = SupportingPartner - fields = ['partner_type', 'institution', - 'institution_acronym', 'institution_address', - 'consortium_members' - ] - - def __init__(self, *args, **kwargs): - super(SupportingPartnerForm, self).__init__(*args, **kwargs) - self.fields['institution_address'].widget = forms.Textarea({'rows': 8, }) - self.fields['consortium_members'].widget.attrs.update( - {'placeholder': 'Please list the names of the institutions within the consortium', }) - self.helper = FormHelper() - self.helper.layout = Layout( - Div( - Div( - Field('institution'), - Field('institution_acronym'), - Field('institution_address'), - css_class='col-6'), - Div( - Field('partner_type'), - Field('consortium_members'), - css_class='col-6'), - css_class='row') - ) - - -class SPBMembershipForm(forms.ModelForm): - class Meta: - model = SPBMembershipAgreement - fields = ['start_date', 'duration', 'offered_yearly_contribution'] - - def __init__(self, *args, **kwargs): - super(SPBMembershipForm, self).__init__(*args, **kwargs) - self.fields['start_date'].widget.attrs.update({'placeholder': 'YYYY-MM-DD'}) - self.fields['offered_yearly_contribution'].initial = 1000 - self.helper = FormHelper() - self.helper.layout = Layout( - Div( - Div( - Field('start_date'), - css_class="col-4"), - Div( - Field('duration'), - css_class="col-2"), - Div( - Field('offered_yearly_contribution'), - HTML('(euros)'), - css_class="col-4"), - css_class="row"), - ) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index f353c60f855feef314a1e4037d91d97f15432629..5a40c6b13b941194b8c15f819a3fe884b16150b2 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -14,6 +14,7 @@ class Command(BaseCommand): # Create Groups SciPostAdmin, created = Group.objects.get_or_create(name='SciPost Administrators') + FinancialAdmin, created = Group.objects.get_or_create(name='Financial Administrators') AdvisoryBoard, created = Group.objects.get_or_create(name='Advisory Board') EditorialAdmin, created = Group.objects.get_or_create(name='Editorial Administrators') EditorialCollege, created = Group.objects.get_or_create(name='Editorial College') @@ -29,6 +30,12 @@ class Command(BaseCommand): # Create Permissions content_type = ContentType.objects.get_for_model(Contributor) + # Supporting Partners + can_manage_SPB, created = Permission.objects.get_or_create( + codename='can_manage_SPB', + name='Can manage Supporting Partners Board', + content_type=content_type) + # Registration and invitations can_vet_registration_requests, created = Permission.objects.get_or_create( codename='can_vet_registration_requests', diff --git a/scipost/migrations/0055_auto_20170519_0937.py b/scipost/migrations/0055_auto_20170519_0937.py new file mode 100644 index 0000000000000000000000000000000000000000..a3818779c198cd915fc642c64d90918024b12a9a --- /dev/null +++ b/scipost/migrations/0055_auto_20170519_0937.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-05-19 07:37 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0054_delete_newsitem'), + ] + + operations = [ + migrations.RemoveField( + model_name='spbmembershipagreement', + name='partner', + ), + migrations.RemoveField( + model_name='supportingpartner', + name='contact_person', + ), + migrations.DeleteModel( + name='SPBMembershipAgreement', + ), + migrations.DeleteModel( + name='SupportingPartner', + ), + ] diff --git a/scipost/models.py b/scipost/models.py index 6cd9f4bca121d80600f13367d54f7edf41ec8118..1b2fa091fee75a4422c4f6186913904e4c3a48a8 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -17,9 +17,7 @@ from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ subject_areas_dict, CONTRIBUTOR_STATUS, TITLE_CHOICES,\ INVITATION_STYLE, INVITATION_TYPE,\ INVITATION_CONTRIBUTOR, INVITATION_FORMAL,\ - AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS,\ - PARTNER_TYPES, PARTNER_STATUS,\ - SPB_MEMBERSHIP_AGREEMENT_STATUS, SPB_MEMBERSHIP_DURATION + AUTHORSHIP_CLAIM_PENDING, AUTHORSHIP_CLAIM_STATUS from .fields import ChoiceArrayField from .managers import FellowManager, ContributorManager @@ -108,76 +106,10 @@ class Contributor(models.Model): self.key_expires = datetime.datetime.now() + datetime.timedelta(days=2) self.save() - def private_info_as_table(self): - template = Template(''' - <table> - <tr><td>Title: </td><td> </td><td>{{ title }}</td></tr> - <tr><td>First name: </td><td> </td><td>{{ first_name }}</td></tr> - <tr><td>Last name: </td><td> </td><td>{{ last_name }}</td></tr> - <tr><td>Email: </td><td> </td><td>{{ email }}</td></tr> - <tr><td>ORCID id: </td><td> </td><td>{{ orcid_id }}</td></tr> - <tr><td>Country of employment: </td><td> </td> - <td>{{ country_of_employment }}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ affiliation }}</td></tr> - <tr><td>Address: </td><td> </td><td>{{ address }}</td></tr> - <tr><td>Personal web page: </td><td> </td><td>{{ personalwebpage }}</td></tr> - <tr><td>Accept SciPost emails: </td><td> </td><td>{{ accepts_SciPost_emails }}</td></tr> - </table> - ''') - context = Context({ - 'title': self.get_title_display(), - 'first_name': self.user.first_name, - 'last_name': self.user.last_name, - 'email': self.user.email, - 'orcid_id': self.orcid_id, - 'country_of_employment': str(self.country_of_employment.name), - 'affiliation': self.affiliation, - 'address': self.address, - 'personalwebpage': self.personalwebpage, - 'accepts_SciPost_emails': self.accepts_SciPost_emails, - }) - return template.render(context) - - def public_info_as_table(self): - """Prints out all publicly-accessible info as a table.""" - - template = Template(''' - <table> - <tr><td>Title: </td><td> </td><td>{{ title }}</td></tr> - <tr><td>First name: </td><td> </td><td>{{ first_name }}</td></tr> - <tr><td>Last name: </td><td> </td><td>{{ last_name }}</td></tr> - <tr><td>ORCID id: </td><td> </td><td>{{ orcid_id }}</td></tr> - <tr><td>Country of employment: </td><td> </td> - <td>{{ country_of_employment }}</td></tr> - <tr><td>Affiliation: </td><td> </td><td>{{ affiliation }}</td></tr> - <tr><td>Personal web page: </td><td> </td><td>{{ personalwebpage }}</td></tr> - </table> - ''') - context = Context({ - 'title': self.get_title_display(), - 'first_name': self.user.first_name, - 'last_name': self.user.last_name, - 'email': self.user.email, - 'orcid_id': self.orcid_id, - 'country_of_employment': str(self.country_of_employment.name), - 'affiliation': self.affiliation, - 'address': self.address, - 'personalwebpage': self.personalwebpage - }) - return template.render(context) - def discipline_as_string(self): # Redundant, to be removed in future return self.get_discipline_display() - def expertises_as_ul(self): - output = '<ul>' - if self.expertises: - for exp in self.expertises: - output += '<li>%s</li>' % subject_areas_dict[exp] - output += '</ul>' - return mark_safe(output) - def expertises_as_string(self): if self.expertises: return ', '.join([subject_areas_dict[exp].lower() for exp in self.expertises]) @@ -400,43 +332,6 @@ class AffiliationObject(models.Model): subunit = models.CharField(max_length=128) -############################# -# Supporting Partners Board # -############################# - -class SupportingPartner(models.Model): - """ - Supporting Partners. - """ - partner_type = models.CharField(max_length=32, choices=PARTNER_TYPES) - status = models.CharField(max_length=16, choices=PARTNER_STATUS) - institution = models.CharField(max_length=256) - institution_acronym = models.CharField(max_length=10) - institution_address = models.CharField(max_length=1000) - consortium_members = models.TextField(blank=True, null=True) - contact_person = models.ForeignKey(Contributor, on_delete=models.CASCADE) - - def __str__(self): - return self.institution_acronym + ' (' + self.get_status_display() + ')' - - -class SPBMembershipAgreement(models.Model): - """ - Agreement for membership of the Supporting Partners Board. - A new instance is created each time an Agreement is made or renewed. - """ - partner = models.ForeignKey(SupportingPartner, on_delete=models.CASCADE) - status = models.CharField(max_length=16, choices=SPB_MEMBERSHIP_AGREEMENT_STATUS) - date_requested = models.DateField() - start_date = models.DateField() - duration = models.DurationField(choices=SPB_MEMBERSHIP_DURATION) - offered_yearly_contribution = models.SmallIntegerField(default=0) - - def __str__(self): - return (str(self.partner) + - ' [' + self.get_duration_display() + - ' from ' + self.start_date.strftime('%Y-%m-%d') + ']') - ###################### # Static info models # diff --git a/scipost/services.py b/scipost/services.py index 395e61e7ea5859a147a2170994c84e43ab6c16c3..d9a03b5295223b71908cc8d7e97d0753f89ddc1a 100644 --- a/scipost/services.py +++ b/scipost/services.py @@ -2,6 +2,8 @@ import feedparser import requests import re +import datetime +import dateutil.parser from django.template import Template, Context from .behaviors import ArxivCallable @@ -9,235 +11,103 @@ from .behaviors import ArxivCallable from strings import arxiv_caller_errormessages -class BaseCaller(object): - '''Base mixin for caller (Arxiv, DOI). - The basic workflow is to initiate the caller, call process() to make the actual call - followed by is_valid() to validate the response of the call. - - An actual caller should inherit at least the following: - > Properties: - - query_base_url - - caller_regex - > Methods: - - process() - ''' - # State of the caller - _is_processed = False - caller_regex = None - errorcode = None - errorvariables = {} - errormessages = {} - identifier_without_vn_nr = '' - identifier_with_vn_nr = '' - metadata = {} - query_base_url = None - target_object = None - version_nr = None - - def __init__(self, target_object, identifier, *args, **kwargs): - '''Initiate the Caller by assigning which object is used - the Arxiv identifier to be called. - - After initiating call in specific order: - - process() - - is_valid() - - Keyword arguments: - target_object -- The model calling the Caller (object) - identifier -- The identifier used for the call (string) - ''' - try: - self._check_valid_caller() - except NotImplementedError as e: - print('Caller invalid: %s' % e) - return - - # Set given arguments - self.target_object = target_object - self.identifier = identifier - self._precheck_if_valid() - super(BaseCaller, self).__init__(*args, **kwargs) - - def _check_identifier(self): - '''Split the given identifier in an article identifier and version number.''' - if not self.caller_regex: - raise NotImplementedError('No regex is set for this caller') - - if re.match(self.caller_regex, self.identifier): - self.identifier_without_vn_nr = self.identifier.rpartition('v')[0] - self.identifier_with_vn_nr = self.identifier - self.version_nr = int(self.identifier.rpartition('v')[2]) +class DOICaller: + def __init__(self, doi_string): + self.doi_string = doi_string + self._call_crosslink() + if self.is_valid: + self._format_data() + + def _call_crosslink(self): + url = 'http://api.crossref.org/works/%s' % self.doi_string + request = requests.get(url) + if request.ok: + self.is_valid = True + self._crossref_data = request.json()['message'] + else: + self.is_valid = False + + def _format_data(self): + data = self._crossref_data + pub_title = data['title'][0] + author_list = ['{} {}'.format(author['given'], author['family']) for author in data['author']] + # author_list is given as a comma separated list of names on the relevant models (Commentary, Submission) + author_list = ", ".join(author_list) + journal = data['container-title'][0] + volume = data.get('volume', '') + pages = self._get_pages(data) + pub_date = self._get_pub_date(data) + + self.data = { + 'pub_title': pub_title, + 'author_list': author_list, + 'journal': journal, + 'volume': volume, + 'pages': pages, + 'pub_date': pub_date, + } + + def _get_pages(self, data): + # For Physical Review + pages = data.get('article-number', '') + # For other journals? + pages = data.get('page', '') + return pages + + def _get_pub_date(self, data): + date_parts = data.get('issued', {}).get('date-parts', {}) + if date_parts: + date_parts = date_parts[0] + year = date_parts[0] + month = date_parts[1] + day = date_parts[2] + pub_date = datetime.date(year, month, day).isoformat() else: - self.errorvariables['identifier_with_vn_nr'] = self.identifier - raise ValueError('bad_identifier') - - def _check_valid_caller(self): - '''Check if all methods and variables are set appropriately''' - if not self.query_base_url: - raise NotImplementedError('No `query_base_url` set') - - def _precheck_duplicate(self): - '''Check if identifier for object already exists.''' - if self.target_object.same_version_exists(self.identifier_with_vn_nr): - raise ValueError('preprint_already_submitted') - - def _precheck_previous_submissions_are_valid(self): - '''Check if previous submitted versions have the appropriate status.''' - try: - self.previous_submissions = self.target_object.different_versions( - self.identifier_without_vn_nr) - except AttributeError: - # Commentaries do not have previous version numbers? - pass - - if self.previous_submissions: - for submission in [self.previous_submissions[0]]: - if submission.status == 'revision_requested': - self.resubmission = True - elif submission.status in ['rejected', 'rejected_visible']: - raise ValueError('previous_submissions_rejected') - else: - raise ValueError('previous_submission_undergoing_refereeing') - - def _precheck_if_valid(self): - '''The master method to perform all checks required during initializing Caller.''' - try: - self._check_identifier() - self._precheck_duplicate() - self._precheck_previous_submissions_are_valid() - # More tests should be called right here...! - except ValueError as e: - self.errorcode = str(e) - - return not self.errorcode - - def _post_process_checks(self): - '''Perform checks after process, to check received data. - - Return: - None -- Raise ValueError with error code for an invalid check. - ''' - pass - - def is_valid(self): - '''Check if the process() call received valid data. - - If `is_valid()` is overwritten in the actual caller, be - sure to call this parent method in the last line! - - Return: - boolean -- True for valid data received. False otherwise. - ''' - if self.errorcode: - return False - if not self._is_processed: - raise ValueError('`process()` should be called first!') - return True - - def process(self): - '''Call to receive data. - - The `process()` should be implemented in the actual - caller be! Be sure to call this parent method in the last line! - ''' - try: - self._post_process_checks() - except ValueError as e: - self.errorcode = str(e) - - self._is_processed = True - - def get_error_message(self, errormessages={}): - '''Return the errormessages for a specific error code, with the possibility to - overrule the default errormessage dictionary for the specific Caller. - ''' - try: - t = Template(errormessages[self.errorcode]) - except KeyError: - t = Template(self.errormessages[self.errorcode]) - return t.render(Context(self.errorvariables)) - - -class DOICaller(BaseCaller): - """Perform a DOI lookup for a given identifier.""" - pass - - -class ArxivCaller(BaseCaller): - """ Performs an Arxiv article lookup for given identifier """ - - # State of the caller - resubmission = False - previous_submissions = [] - errormessages = arxiv_caller_errormessages - errorvariables = { - 'arxiv_journal_ref': '', - 'arxiv_doi': '', - 'identifier_with_vn_nr': '' - } - arxiv_journal_ref = '' - arxiv_doi = '' - metadata = {} + pub_date = '' + + return pub_date + + +class ArxivCaller: query_base_url = 'http://export.arxiv.org/api/query?id_list=%s' - caller_regex = "^[0-9]{4,}.[0-9]{4,5}v[0-9]{1,2}$" - - def __init__(self, target_object, identifier): - if not issubclass(target_object, ArxivCallable): - raise TypeError('Given target_object is not an ArxivCallable object.') - super(ArxivCaller, self).__init__(target_object, identifier) - - def process(self): - '''Do the actual call the receive Arxiv information.''' - if self.errorcode: - return - - queryurl = (self.query_base_url % self.identifier_with_vn_nr) - - try: - self._response = requests.get(queryurl, timeout=4.0) - except requests.ReadTimeout: - self.errorcode = 'arxiv_timeout' - return - except requests.ConnectionError: - self.errorcode = 'arxiv_timeout' - return - - self._response_content = feedparser.parse(self._response.content) - - super(ArxivCaller, self).process() - - def _post_process_checks(self): - # Check if response has at least one entry - if self._response.status_code == 400 or 'entries' not in self._response_content: - raise ValueError('arxiv_bad_request') - - # Check if preprint exists - if not self.preprint_exists(): - raise ValueError('preprint_does_not_exist') - - # Check via journal ref if already published - self.arxiv_journal_ref = self.published_journal_ref() - self.errorvariables['arxiv_journal_ref'] = self.arxiv_journal_ref - if self.arxiv_journal_ref: - raise ValueError('paper_published_journal_ref') - - # Check via DOI if already published - self.arxiv_doi = self.published_doi() - self.errorvariables['arxiv_doi'] = self.arxiv_doi - if self.arxiv_doi: - raise ValueError('paper_published_doi') - - self.metadata = self._response_content - - def preprint_exists(self): - return 'title' in self._response_content['entries'][0] - - def published_journal_ref(self): - if 'arxiv_journal_ref' in self._response_content['entries'][0]: - return self._response_content['entries'][0]['arxiv_journal_ref'] - return None - - def published_doi(self): - if 'arxiv_doi' in self._response_content['entries'][0]: - return self._response_content['entries'][0]['arxiv_doi'] - return None + + def __init__(self, identifier): + self.identifier = identifier + self._call_arxiv() + if self.is_valid: + self._format_data() + + def _call_arxiv(self): + url = self.query_base_url % self.identifier + request = requests.get(url) + 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 + + def _format_data(self): + data = self._arxiv_data + pub_title = data['title'] + author_list = [author['name'] for author in data['authors']] + # author_list is given as a comma separated list of names on the relevant models (Commentary, Submission) + author_list = ", ".join(author_list) + arxiv_link = data['id'] + abstract = data['summary'] + pub_date = dateutil.parser.parse(data['published']).date() + + 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, + } + + def _search_result_present(self, data): + return 'title' in data diff --git a/scipost/templates/scipost/_assignments_summary_as_td.html b/scipost/templates/scipost/_assignments_summary_as_td.html new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scipost/templates/scipost/_expertises_as_ul.html b/scipost/templates/scipost/_expertises_as_ul.html new file mode 100644 index 0000000000000000000000000000000000000000..16150eeff9de2f19d6ce3483837ec9dd6ee6c861 --- /dev/null +++ b/scipost/templates/scipost/_expertises_as_ul.html @@ -0,0 +1,9 @@ +{% load scipost_extras %} + +<ul> + {% for expertise in contributor.expertises %} + <li> + {{ expertise|get_specialization_display }} + </li> + {% endfor %} +</ul> diff --git a/scipost/templates/scipost/_private_info_as_table.html b/scipost/templates/scipost/_private_info_as_table.html new file mode 100644 index 0000000000000000000000000000000000000000..04d525681719b0e696584c1584c9baeaaa12d04b --- /dev/null +++ b/scipost/templates/scipost/_private_info_as_table.html @@ -0,0 +1,13 @@ +<table> + <tr><td>Title: </td><td> </td><td>{{ contributor.get_title_display }}</td></tr> + <tr><td>First name: </td><td> </td><td>{{ contributor.user.first_name }}</td></tr> + <tr><td>Last name: </td><td> </td><td>{{ contributor.user.last_name }}</td></tr> + <tr><td>Email: </td><td> </td><td>{{ contributor.user.email }}</td></tr> + <tr><td>ORCID id: </td><td> </td><td>{{ contributor.orcid_id }}</td></tr> + <tr><td>Country of employment: </td><td> </td> + <td>{{ contributor.country_of_employment.name }}</td></tr> + <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation }}</td></tr> + <tr><td>Address: </td><td> </td><td>{{ contributor.address }}</td></tr> + <tr><td>Personal web page: </td><td> </td><td>{{ contributor.personalwebpage }}</td></tr> + <tr><td>Accept SciPost emails: </td><td> </td><td>{{ contributor.accepts_SciPost_emails }}</td></tr> +</table> diff --git a/scipost/templates/scipost/_public_info_as_table.html b/scipost/templates/scipost/_public_info_as_table.html new file mode 100644 index 0000000000000000000000000000000000000000..964e8098875162e47bc4b1fa7a16f1687b1b29cf --- /dev/null +++ b/scipost/templates/scipost/_public_info_as_table.html @@ -0,0 +1,10 @@ +<table> + <tr><td>Title: </td><td> </td><td>{{ contributor.get_title_display }}</td></tr> + <tr><td>First name: </td><td> </td><td>{{ contributor.user.first_name }}</td></tr> + <tr><td>Last name: </td><td> </td><td>{{ contributor.user.last_name }}</td></tr> + <tr><td>ORCID id: </td><td> </td><td>{{ contributor.orcid_id }}</td></tr> + <tr><td>Country of employment: </td><td> </td> + <td>{{ contributor.country_of_employment.name }}</td></tr> + <tr><td>Affiliation: </td><td> </td><td>{{ contributor.affiliation }}</td></tr> + <tr><td>Personal web page: </td><td> </td><td>{{ contributor.personalwebpage }}</td></tr> +</table> diff --git a/scipost/templates/scipost/base.html b/scipost/templates/scipost/base.html index 1b34a3cbd5aeeb8e2ecff7a934714a18df91fbc4..827cdc5f951e64562772aaa598f4ef12b72bb1fa 100644 --- a/scipost/templates/scipost/base.html +++ b/scipost/templates/scipost/base.html @@ -56,7 +56,7 @@ processEscapes: true }}); </script> - <script type="text/javascript" async src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-MML-AM_CHTML"></script> + <script type="text/javascript" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-MML-AM_CHTML"></script> {% render_bundle 'main' 'js' %} {% render_bundle 'bootstrap' 'js' %} diff --git a/scipost/templates/scipost/contributor_info.html b/scipost/templates/scipost/contributor_info.html index 7f687bb6bb53ed9fa0fd0d9470435a9e42eee98b..de3c1c059991c66714ce4bd67a331014f77190c1 100644 --- a/scipost/templates/scipost/contributor_info.html +++ b/scipost/templates/scipost/contributor_info.html @@ -15,7 +15,8 @@ </div> </div> -{{ contributor.public_info_as_table }} +{% include "scipost/_public_info_as_table.html" with contributor=contributor %} + <br> {% if contributor_publications %} {# <hr>#} diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index 0fdefd67db08f4abf334fe25cb86c9b37b9239c1..6f3c572d5f36abdf5f2296792252f41fb2af432a 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -81,14 +81,14 @@ <div class="row"> <div class="col-md-6"> <h3>Your personal details:</h3> - {{ contributor.private_info_as_table }} + {% include "scipost/_private_info_as_table.html" with contributor=contributor %} <h3 class="mt-3">Your main discipline:</h3> <ul><li>{{ contributor.discipline_as_string }}</li></ul> <h3 class="mt-3">Your expertises:</h3> {% if contributor.expertises %} - {{ contributor.expertises_as_ul }} + {% include "scipost/_expertises_as_ul.html" with contributor=contributor %} {% else %} <p>You haven't listed your expertise(s).<br/> Do so by <a href="{% url 'scipost:update_personal_data' %}">updating your personal data</a> diff --git a/scipost/templates/scipost/vet_registration_requests.html b/scipost/templates/scipost/vet_registration_requests.html index 8b5630bff03f7b936b70fb4b25a051229a162d7e..532269903f507b3fe4361240ff28528ff6e37d98 100644 --- a/scipost/templates/scipost/vet_registration_requests.html +++ b/scipost/templates/scipost/vet_registration_requests.html @@ -41,7 +41,7 @@ $(function() { {% if not forloop.first %}<hr class="small">{% endif %} <div class="row"> <div class="col-md-4"> - {{ contributor_to_vet.private_info_as_table }} + {% include "scipost/_private_info_as_table.html" with contributor=contributor %} </div> <div class="col-md-8"> <form action="{% url 'scipost:vet_registration_request_ack' contributor_id=contributor_to_vet.id %}" method="post"> diff --git a/scipost/test_services.py b/scipost/test_services.py index e724b7856b0d5ec1a280239d440a66c1b758885b..727a20213e84256e79fc3d85f875df77f9be56a7 100644 --- a/scipost/test_services.py +++ b/scipost/test_services.py @@ -1,46 +1,62 @@ +import datetime + from django.test import TestCase -from .services import ArxivCaller +from .services import ArxivCaller, DOICaller from submissions.models import Submission class ArxivCallerTest(TestCase): - - def test_correct_lookup(self): - caller = ArxivCaller(Submission, '1611.09574v1') - - caller.process() - - self.assertEqual(caller.is_valid(), True) - self.assertIn('entries', caller.metadata) - - def test_errorcode_for_non_existing_paper(self): - caller = ArxivCaller(Submission, '2611.09574v1') - - caller.process() - self.assertEqual(caller.is_valid(), False) - self.assertEqual(caller.errorcode, 'preprint_does_not_exist') - - def test_errorcode_for_bad_request(self): - caller = ArxivCaller(Submission, '161109574v1') - - caller.process() - self.assertEqual(caller.is_valid(), False) - self.assertEqual(caller.errorcode, 'arxiv_bad_request') - - def test_errorcode_for_already_published_journal_ref(self): - caller = ArxivCaller(Submission, '1412.0006v1') - - caller.process() - self.assertEqual(caller.is_valid(), False) - self.assertEqual(caller.errorcode, 'paper_published_journal_ref') - self.assertNotEqual(caller.arxiv_journal_ref, '') - - def test_errorcode_no_version_nr(self): - # Should be already caught in form validation - caller = ArxivCaller(Submission, '1412.0006') - - caller.process() - self.assertEqual(caller.is_valid(), False) - self.assertEqual(caller.errorcode, 'bad_identifier') + def test_identifier_new_style(self): + caller = ArxivCaller('1612.07611v1') + self.assertTrue(caller.is_valid) + correct_data = { + 'pub_abstract': 'The Berezinskii-Kosterlitz-Thouless (BKT) transitions of the six-state clock\nmodel on the square lattice are investigated by means of the corner-transfer\nmatrix renormalization group method. The classical analogue of the entanglement\nentropy $S( L, T )$ is calculated for $L$ by $L$ square system up to $L = 129$,\nas a function of temperature $T$. The entropy has a peak at $T = T^{*}_{~}( L\n)$, where the temperature depends on both $L$ and boundary conditions. Applying\nthe finite-size scaling to $T^{*}_{~}( L )$ and assuming the presence of BKT\ntransitions, the transition temperature is estimated to be $T_1^{~} = 0.70$ and\n$T_2^{~} = 0.88$. The obtained results agree with previous analyses. It should\nbe noted that no thermodynamic function is used in this study.', 'author_list': ['Roman KrÄmár', 'Andrej Gendiar', 'Tomotoshi Nishino'], 'arxiv_link': 'http://arxiv.org/abs/1612.07611v1', 'pub_title': 'Phase transition of the six-state clock model observed from the\n entanglement entropy', 'pub_date': datetime.date(2016, 12, 22) + } + self.assertEqual(caller.data, correct_data) + + def test_identifier_old_style(self): + caller = ArxivCaller('cond-mat/0612480') + self.assertTrue(caller.is_valid) + correct_data = { + 'author_list': ['Kouji Ueda', 'Chenglong Jin', 'Naokazu Shibata', 'Yasuhiro Hieida', 'Tomotoshi Nishino'], 'pub_date': datetime.date(2006, 12, 19), 'arxiv_link': 'http://arxiv.org/abs/cond-mat/0612480v2', 'pub_abstract': 'A kind of least action principle is introduced for the discrete time\nevolution of one-dimensional quantum lattice models. Based on this principle,\nwe obtain an optimal condition for the matrix product states on succeeding time\nslices generated by the real-time density matrix renormalization group method.\nThis optimization can also be applied to classical simulations of quantum\ncircuits. We discuss the time reversal symmetry in the fully optimized MPS.', 'pub_title': 'Least Action Principle for the Real-Time Density Matrix Renormalization\n Group' + } + self.assertEqual(caller.data, correct_data) + + def valid_but_nonexistent_identifier(self): + caller = ArxivCaller('1613.07611v1') + self.assertEqual(caller.is_valid, False) + + +class DOICallerTest(TestCase): + def test_works_for_physrev_doi(self): + caller = DOICaller('10.1103/PhysRevB.92.214427') + correct_data = { + 'pub_date': '2015-12-18', + 'journal': 'Physical Review B', + 'pages': '', + 'author_list': [ + 'R. Vlijm', 'M. Ganahl', 'D. Fioretto', 'M. Brockmann', 'M. Haque', 'H. G. Evertz', 'J.-S. Caux'], + 'volume': '92', + 'pub_title': 'Quasi-soliton scattering in quantum spin chains' + } + self.assertTrue(caller.is_valid) + self.assertEqual(caller.data, correct_data) + + def test_works_for_scipost_doi(self): + caller = DOICaller('10.21468/SciPostPhys.2.2.012') + correct_data = { + 'pub_date': '2017-04-04', + 'journal': 'SciPost Physics', + 'pub_title': 'One-particle density matrix of trapped one-dimensional impenetrable bosons from conformal invariance', + 'pages': '', + 'volume': '2', + 'author_list': ['Yannis Brun', 'Jerome Dubail'] + } + self.assertTrue(caller.is_valid) + self.assertEqual(caller.data, correct_data) + + def test_valid_but_non_existent_doi(self): + caller = DOICaller('10.21468/NonExistentJournal.2.2.012') + self.assertEqual(caller.is_valid, False) diff --git a/scipost/urls.py b/scipost/urls.py index d12d2fc75c8e25a73214656aff7faab8e44f8238..5b8809d94c62da572e14e96c9f00ed5d6e11769a 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -209,14 +209,4 @@ urlpatterns = [ TemplateView.as_view(template_name='scipost/howto_production.html'), name='howto_production'), - - ############################# - # Supporting Partners Board # - ############################# - - url(r'^supporting_partners$', views.supporting_partners, - name='supporting_partners'), - url(r'^SPB_membership_request$', views.SPB_membership_request, - name='SPB_membership_request'), - ] diff --git a/scipost/views.py b/scipost/views.py index 43d629d2551f73d26672ebf0983576e95a886fc2..92358cc472dce943b1a419266991c270ef60c4ce 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -25,14 +25,12 @@ from guardian.decorators import permission_required from .constants import SCIPOST_SUBJECT_AREAS, subject_areas_raw_dict, SciPost_from_addresses_dict from .models import Contributor, CitationNotification, UnavailabilityPeriod,\ DraftInvitation, RegistrationInvitation,\ - AuthorshipClaim, SupportingPartner, SPBMembershipAgreement,\ - EditorialCollege, EditorialCollegeFellowship + AuthorshipClaim, EditorialCollege, EditorialCollegeFellowship from .forms import AuthenticationForm, DraftInvitationForm, UnavailabilityPeriodForm,\ RegistrationForm, RegistrationInvitationForm, AuthorshipClaimForm,\ ModifyPersonalMessageForm, SearchForm, VetRegistrationForm, reg_ref_dict,\ UpdatePersonalDataForm, UpdateUserDataForm, PasswordChangeForm,\ - EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm,\ - SupportingPartnerForm, SPBMembershipForm + EmailGroupMembersForm, EmailParticularForm, SendPrecookedEmailForm from .utils import Utils, EMAIL_FOOTER, SCIPOST_SUMMARY_FOOTER, SCIPOST_SUMMARY_FOOTER_HTML from commentaries.models import Commentary @@ -817,7 +815,8 @@ def personal_page(request): nr_thesislink_requests_to_vet = 0 nr_authorship_claims_to_vet = 0 if contributor.is_VE(): - nr_commentary_page_requests_to_vet = Commentary.objects.filter(vetted=False).count() + nr_commentary_page_requests_to_vet = (Commentary.objects.awaiting_vetting() + .exclude(requested_by=contributor).count()) nr_comments_to_vet = Comment.objects.filter(status=0).count() nr_thesislink_requests_to_vet = ThesisLink.objects.filter(vetted=False).count() nr_authorship_claims_to_vet = AuthorshipClaim.objects.filter(status='0').count() @@ -1274,58 +1273,6 @@ def Fellow_activity_overview(request, Fellow_id=None): return render(request, 'scipost/Fellow_activity_overview.html', context) -############################# -# Supporting Partners Board # -############################# - -def supporting_partners(request): - prospective_agreements = SPBMembershipAgreement.objects.filter( - status='Submitted').order_by('date_requested') - context = {'prospective_partners': prospective_agreements, } - return render(request, 'scipost/supporting_partners.html', context) - - -@login_required -def SPB_membership_request(request): - errormessage = '' - if request.method == 'POST': - SP_form = SupportingPartnerForm(request.POST) - membership_form = SPBMembershipForm(request.POST) - if SP_form.is_valid() and membership_form.is_valid(): - partner = SupportingPartner( - partner_type=SP_form.cleaned_data['partner_type'], - status='Prospective', - institution=SP_form.cleaned_data['institution'], - institution_acronym=SP_form.cleaned_data['institution_acronym'], - institution_address=SP_form.cleaned_data['institution_address'], - contact_person=request.user.contributor, - ) - partner.save() - agreement = SPBMembershipAgreement( - partner=partner, - status='Submitted', - date_requested=timezone.now().date(), - start_date=membership_form.cleaned_data['start_date'], - duration=membership_form.cleaned_data['duration'], - offered_yearly_contribution=membership_form.cleaned_data['offered_yearly_contribution'], - ) - agreement.save() - ack_message = ('Thank you for your SPB Membership request. ' - 'We will get back to you in the very near future ' - 'with details of the proposed agreement.') - context = {'ack_message': ack_message, } - return render(request, 'scipost/acknowledgement.html', context) - else: - errormessage = 'The form was not filled properly.' - - else: - SP_form = SupportingPartnerForm() - membership_form = SPBMembershipForm() - context = {'errormessage': errormessage, - 'SP_form': SP_form, - 'membership_form': membership_form, } - return render(request, 'scipost/SPB_membership_request.html', context) - class AboutView(ListView): model = EditorialCollege diff --git a/strings/__init__.py b/strings/__init__.py index acb0df6ceafab9bb8a85bae4049a9ccb7fdc8cec..614455a3452553b019e2f375efa0ad241bd8f690 100644 --- a/strings/__init__.py +++ b/strings/__init__.py @@ -10,6 +10,28 @@ acknowledge_request_commentary = ( acknowledge_submit_comment = ( "Thank you for contributing a Comment. It will soon be vetted by an Editor." ) +acknowledge_doi_query = "Crossref query by DOI successful." +acknowledge_arxiv_query = "Arxiv query successful." + +doi_query_placeholder = 'ex.: 10.21468/00.000.000000' +doi_query_help_text = ( + 'For published papers, you can prefill the form (except for domain, subject area and abstract) using the DOI. ' + "(Give the DOI as 10.[4 to 9 digits]/[string], without prefix, as per the placeholder)." +) +doi_query_invalid = ( + "DOI does not match the expression supplied by CrossRef. Either it is very old or you made a mistake. " + "If you are sure it is correct, please enter the metadata manually. Sorry for the inconvenience." +) + +arxiv_query_placeholder = ( + "new style: YYMM.####(#)v#(#) or " + "old style: cond-mat/YYMM###v#(#)" +) +arxiv_query_help_text = ( + "For preprints, you can prefill the form using the arXiv identifier. " + "Give the identifier without prefix and do not forget the version number, as per the placeholder." +) +arxiv_query_invalid = 'ArXiv identifier is invalid. Did you include a version number?' # Arxiv response is not valid arxiv_caller_errormessages = { @@ -21,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 b49f185de05e6050d7eba0075423db59f14458c4..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,135 +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() @@ -164,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): @@ -1290,6 +1218,10 @@ def fix_College_decision(request, rec_id): if recommendation.recommendation in [1, 2, 3]: # Publish as Tier I, II or III recommendation.submission.status = 'accepted' + # Create a ProductionStream object + prodstream = ProductionStream(submission=recommendation.submission, + opened=timezone.now()) + prodstream.save() elif recommendation.recommendation == -3: # Reject recommendation.submission.status = 'rejected' diff --git a/templates/email/submission_cycle_reinvite_referee.html b/templates/email/submission_cycle_reinvite_referee.html index 3ae8ab59c207d451b19c0b3ad4e0da54de4ebd63..f776b1bd3329c58dd2f8d3fa303403328adf1fe4 100644 --- a/templates/email/submission_cycle_reinvite_referee.html +++ b/templates/email/submission_cycle_reinvite_referee.html @@ -4,7 +4,7 @@ The authors of submission\n\n {{invitation.submission.title}} by {{invitation.submission.author_list}} \n\n -have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.last_name}}, we would like to invite you to quickly review this new version.\n +have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version.\n Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days).\n\n If you accept, your report can be submitted by simply clicking on the "Contribute a Report" link on the Submission's Page before the reporting deadline (currently set at {{invitation.submission.reporting_deadline|date:'N j, Y'}}; your report will be automatically recognized as an invited report).\n\n diff --git a/templates/email/submission_cycle_reinvite_referee_html.html b/templates/email/submission_cycle_reinvite_referee_html.html index 84ceeb3ff14ee0d82279da763c9fd7ff30a5cc53..08db162758be0ab0ed123a02ccd3b0667456d828 100644 --- a/templates/email/submission_cycle_reinvite_referee_html.html +++ b/templates/email/submission_cycle_reinvite_referee_html.html @@ -11,7 +11,7 @@ <br> (<a href="https://scipost.org{{invitation.submission.get_absolute_url}}">see on SciPost.org</a>) <p> - have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.last_name}}, we would like to invite you to quickly review this new version. + have resubmitted their manuscript to SciPost. On behalf of the Editor-in-charge {{invitation.submission.editor_in_charge.get_title_display}} {{invitation.submission.editor_in_charge.user.last_name}}, we would like to invite you to quickly review this new version. Please accept or decline the invitation (login required) as soon as possible (ideally within the next 2 days). </p> <p>