diff --git a/commentaries/factories.py b/commentaries/factories.py index 940e958cb31ba3d2039c0e44c8ce1f58640930df..1f0110a5d0a44caa1ffb3519f8f86a3703a2871d 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..0af8bc931ce0914fa1842ed03e30607ed194ebb4 100644 --- a/commentaries/forms.py +++ b/commentaries/forms.py @@ -2,107 +2,154 @@ 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 save(self, *args, **kwargs): + self.instance.parse_links_into_urls() + return super().save(self, *args, **kwargs) + + +class RequestArxivPreprintForm(RequestCommentaryForm): + class Meta(RequestCommentaryForm.Meta): + model = Commentary + fields = RequestCommentaryForm.Meta.fields + ['arxiv_identifier'] 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}) + 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 - 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.') + # 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) + - def get_existing_commentary(self): - """Get Commentary if found after validation""" - return self.existing_commentary +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 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): 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/request_arxiv_preprint.html b/commentaries/templates/commentaries/request_arxiv_preprint.html new file mode 100644 index 0000000000000000000000000000000000000000..44ef80f88251608066b436348d7ef430db147308 --- /dev/null +++ b/commentaries/templates/commentaries/request_arxiv_preprint.html @@ -0,0 +1,35 @@ +{% extends 'scipost/base.html' %} +{% load bootstrap %} +{% load scipost_extras %} + +{% block pagetitle %}: request Commentary{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="page-header">Request Activation of a Commentary Page</h1> + </div> +</div> + +<div class="row"> + <div class="col-12 col-md-8"> + <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> + +<div class="row"> + <div class="col-12 col-md-8"> + <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..c89649093d40e31bd66aef4d366d9ed2033f5c46 100644 --- a/commentaries/templates/commentaries/request_commentary.html +++ b/commentaries/templates/commentaries/request_commentary.html @@ -8,112 +8,21 @@ {% 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> </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 submit a published article</a> + </li> + <li> + <a href="{% url 'commentaries:request_arxiv_preprint' %}">Click here to submit 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..b0bf7365a5956c3f77fa1559e83931758fb0fc60 --- /dev/null +++ b/commentaries/templates/commentaries/request_published_article.html @@ -0,0 +1,35 @@ +{% extends 'scipost/base.html' %} +{% load bootstrap %} +{% load scipost_extras %} + +{% block pagetitle %}: request Commentary{% endblock pagetitle %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="page-header">Request Activation of a Commentary Page</h1> + </div> +</div> + +<div class="row"> + <div class='col-12 col-md-8'> + <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-12 col-md-8'> + <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/test_forms.py b/commentaries/test_forms.py index 8733064ae2267d07111ed7c9fe0c1662cc85fb3f..75d5a21dffce0a9268e62ef106c15dd9664ca97c 100644 --- a/commentaries/test_forms.py +++ b/commentaries/test_forms.py @@ -1,14 +1,98 @@ +import re + from django.test import TestCase from common.helpers import model_form_data from scipost.factories import UserFactory -from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory -from .forms import RequestCommentaryForm, VetCommentaryForm +from .factories import VettedCommentaryFactory, UnvettedCommentaryFactory, UnvettedArxivPreprintCommentaryFactory +from .forms import RequestCommentaryForm, 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() + + 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() + + 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() @@ -20,6 +104,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 +155,53 @@ 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() + 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() + 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..dbb0bb6c61464681abcf3e41acd0da83b533d17d 100644 --- a/commentaries/test_views.py +++ b/commentaries/test_views.py @@ -2,38 +2,74 @@ 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.commentary_instance = UnvettedCommentaryFactory.build(requested_by=ContributorFactory()) + 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 = UserFactory() + + 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) + + +class RequestArxivPreprintTest(TestCase): + def setUp(self): + add_groups_and_permissions() + self.target = reverse('commentaries:request_arxiv_preprint') + self.commentary_instance = UnvettedArxivPreprintCommentaryFactory.build(requested_by=ContributorFactory()) + 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(self): + request = RequestFactory().post(self.target, self.valid_form_data) + request.user = UserFactory() + + 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) class VetCommentaryRequestsTest(TestCase): """Test cases for `vet_commentary_requests` view method""" @@ -77,12 +113,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 +129,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 +141,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 +150,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} diff --git a/commentaries/urls.py b/commentaries/urls.py index a1b5ba5ec119853ae337ab52c7213cb4feadea8f..03be372fb2a4229fc4ca73cb866c92ba2a56ee1e 100644 --- a/commentaries/urls.py +++ b/commentaries/urls.py @@ -21,10 +21,13 @@ 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]+)$', diff --git a/commentaries/views.py b/commentaries/views.py index d8554ec92e92690cf12c7c6e27802183d3f2154a..4c50ca0eb0bfb787ec9daa001339b78c54ee5c05 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -13,10 +13,11 @@ from django.template.loader import render_to_string from django.views.generic.edit import CreateView, FormView 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, \ + CommentarySearchForm, RequestPublishedArticleForm, RequestArxivPreprintForm from comments.models import Comment from comments.forms import CommentForm @@ -26,180 +27,79 @@ 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 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) @@ -211,7 +111,6 @@ def vet_commentary_requests(request): context = {'contributor': contributor, 'commentary_to_vet': commentary_to_vet, 'form': form} return render(request, 'commentaries/vet_commentary_requests.html', context) - @permission_required('scipost.can_vet_commentary_requests', raise_exception=True) def vet_commentary_request_ack(request, commentary_id): if request.method == 'POST': @@ -270,6 +169,7 @@ 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.''' diff --git a/scipost/services.py b/scipost/services.py index 395e61e7ea5859a147a2170994c84e43ab6c16c3..17ca3811f608486fe1a580e636a07591343d1e28 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,99 @@ 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) + arxiv_data = feedparser.parse(request.content)['entries'][0] + if self._search_result_present(arxiv_data): + self.is_valid = True + self._arxiv_data = arxiv_data + 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, + 'author_list': author_list, + 'arxiv_link': arxiv_link, + 'pub_abstract': abstract, + 'pub_date': pub_date, + } + + def _search_result_present(self, data): + return 'title' in data 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/strings/__init__.py b/strings/__init__.py index acb0df6ceafab9bb8a85bae4049a9ccb7fdc8cec..51115395b4c01978c62b95c036d3f9a5e7445cfc 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 = {