diff --git a/invitations/admin.py b/invitations/admin.py index 59092e3c025f2d394d3a28100190ff2402eedb37..068ed5f3598d9def5645964aa8ae308ec7d23f64 100644 --- a/invitations/admin.py +++ b/invitations/admin.py @@ -20,7 +20,7 @@ admin.site.register(RegistrationInvitation, RegistrationInvitationAdmin) class CitationNotificationAdmin(admin.ModelAdmin): date_hierarchy = 'date_sent' search_fields = ['invitation__first_name', 'invitation__last_name', - 'contributor__first_name', 'contributor__last_name'] + 'contributor__user__first_name', 'contributor__user__last_name'] list_display = ['__str__', 'created_by', 'date_sent', 'processed'] list_filter = ['processed'] diff --git a/invitations/forms.py b/invitations/forms.py index 98da5c53684c3ff8be40188af9990244f8331d8b..d3e299992bed5d5fbc0147f778647042e8389e41 100644 --- a/invitations/forms.py +++ b/invitations/forms.py @@ -126,6 +126,66 @@ class RegistrationInvitationAddCitationForm(AcceptRequestMixin, forms.ModelForm) return self.instance +class RegistrationInvitationMergeForm(AcceptRequestMixin, forms.ModelForm): + """Merge RegistrationInvitations. + + This form will merge the instance with any other RegistrationInvitation selected + into a single RegistrationInvitation. + """ + + invitation = forms.ModelChoiceField(queryset=RegistrationInvitation.objects.none(), + label="Invitation to merge with") + + class Meta: + model = RegistrationInvitation + fields = () + + def __init__(self, *args, **kwargs): + """Update queryset according to the passed instance.""" + super().__init__(*args, **kwargs) + self.fields['invitation'].queryset = RegistrationInvitation.objects.no_response().filter( + last_name__icontains=self.instance.last_name).exclude(id=self.instance.id) + + def save(self, *args, **kwargs): + """Merge the two RegistationInvitations into one.""" + if kwargs.get('commit', True): + # Pick the right Invitation, with the most up-to-date invitation_key + selected_invitation = self.cleaned_data['invitation'] + if not selected_invitation.date_sent_last: + # Selected Invitation has never been sent yet. + leading_invitation = self.instance + deprecated_invitation = selected_invitation + elif not self.instance.date_sent_last: + # Instance has never been sent yet. + leading_invitation = selected_invitation + deprecated_invitation = self.instance + elif selected_invitation.date_sent_last > self.instance.date_sent_last: + # Lastest reminder: selected Invitation + leading_invitation = selected_invitation + deprecated_invitation = self.instance + else: + # Lastest reminder: instance + leading_invitation = self.instance + deprecated_invitation = selected_invitation + + # Move CitationNotification to the new leading Invitation + deprecated_invitation.citation_notifications.update(invitation=leading_invitation) + leading_invitation.times_sent += deprecated_invitation.times_sent # Update counts + leading_invitation.save() + + qs_contributor = deprecated_invitation.citation_notifications.filter( + contributor__isnull=False).values_list('contributor', flat=True) + if qs_contributor: + if not leading_invitation.citation_notifications.filter(contributor__isnull=False): + # Contributor is already assigned in "old" RegistrationInvitation, copy it. + leading_invitation.citation_notifications.filter(contributor=qs_contributor[0]) + + # Magic. + deprecated_invitation.delete() + return self.instance + + + class RegistrationInvitationForm(AcceptRequestMixin, forms.ModelForm): cited_in_submissions = AutoCompleteSelectMultipleField('submissions_lookup', required=False) cited_in_publications = AutoCompleteSelectMultipleField('publication_lookup', required=False) diff --git a/invitations/migrations/0013_auto_20180414_2053.py b/invitations/migrations/0013_auto_20180414_2053.py new file mode 100644 index 0000000000000000000000000000000000000000..8537b33a834a5aba7e52c4f69a5f36901b36828d --- /dev/null +++ b/invitations/migrations/0013_auto_20180414_2053.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 18:53 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('invitations', '0012_auto_20180220_2120'), + ] + + operations = [ + migrations.AlterField( + model_name='citationnotification', + name='invitation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='citation_notifications', to='invitations.RegistrationInvitation'), + ), + ] diff --git a/invitations/models.py b/invitations/models.py index fc42000e0066051c1b2372bc44f7f923fa8839e0..9d1a8b8b5f687cab32aef07255f54f3bec428db1 100644 --- a/invitations/models.py +++ b/invitations/models.py @@ -98,7 +98,7 @@ class RegistrationInvitation(models.Model): class CitationNotification(models.Model): invitation = models.ForeignKey('invitations.RegistrationInvitation', - on_delete=models.SET_NULL, + on_delete=models.CASCADE, null=True, blank=True) contributor = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE, diff --git a/invitations/templates/invitations/registrationinvitation_form_merge.html b/invitations/templates/invitations/registrationinvitation_form_merge.html new file mode 100644 index 0000000000000000000000000000000000000000..3c284b5102cc9c3a5a7c25e9feeba03adf52da0c --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_merge.html @@ -0,0 +1,34 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Edit Registration Invitation{% endblock pagetitle %} + +{% load scipost_extras %} +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Edit</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Registration Invitation {{ object.id }}</h1> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <button type="submit" class="btn btn-primary" name="save" value="save">Merge</button> + </form> + </div> +</div> + +{% endblock %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} diff --git a/invitations/templates/partials/invitations/registrationinvitation_table.html b/invitations/templates/partials/invitations/registrationinvitation_table.html index 1e85df48e5d4567cac19a3196e23f34ea2d32f2b..86ac4b72bca1963997f2cb59f82c325da7eb702d 100644 --- a/invitations/templates/partials/invitations/registrationinvitation_table.html +++ b/invitations/templates/partials/invitations/registrationinvitation_table.html @@ -53,6 +53,7 @@ </li> {% endfor %} <li><a href="{% url 'invitations:add_citation' invitation.id %}">Add new Citation to Invitation</a></li> + <li><a href="{% url 'invitations:merge' invitation.id %}">Merge this Invitation</a></li> </ul> </td> {% else %} diff --git a/invitations/urls.py b/invitations/urls.py index 5f69269f97c6686a4b19c097d58a38c5eac72124..f9149a5dd25b07779c9f8388ee13fdc8c8a1cc7c 100644 --- a/invitations/urls.py +++ b/invitations/urls.py @@ -16,6 +16,8 @@ urlpatterns = [ name='add_citation'), url(r'^(?P<pk>[0-9]+)/delete$', views.RegistrationInvitationsDeleteView.as_view(), name='delete'), + url(r'^(?P<pk>[0-9]+)/merge$', views.RegistrationInvitationsMergeView.as_view(), + name='merge'), url(r'^(?P<pk>[0-9]+)/mark/(?P<label>sent)$', views.RegistrationInvitationsMarkView.as_view(), name='mark'), url(r'^(?P<pk>[0-9]+)/map_to_contributor/(?P<contributor_id>[0-9]+)/$', diff --git a/invitations/views.py b/invitations/views.py index 3caa687f791cdd794f04df1d860c29ebef9f7fa0..86a1f02d829a1f87cafdf3d9acaf45d3d64850f8 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -13,7 +13,8 @@ from django.views.generic.edit import UpdateView, DeleteView from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm,\ RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\ CitationNotificationForm, SuggestionSearchForm, RegistrationInvitationFilterForm,\ - CitationNotificationProcessForm, RegistrationInvitationAddCitationForm + CitationNotificationProcessForm, RegistrationInvitationAddCitationForm,\ + RegistrationInvitationMergeForm from .mixins import RequestArgumentMixin, SaveAndSendFormMixin, SendMailFormMixin from .models import RegistrationInvitation, CitationNotification @@ -54,7 +55,6 @@ class RegistrationInvitationsFellowView(RegistrationInvitationsView): queryset = RegistrationInvitation.objects.no_response().for_fellows() template_name = 'invitations/registrationinvitation_list_fellows.html' - class CitationNotificationsView(PermissionsMixin, ListView): permission_required = 'scipost.can_manage_registration_invitations' queryset = CitationNotification.objects.unprocessed().prefetch_related( @@ -162,6 +162,14 @@ class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin, return qs +class RegistrationInvitationsMergeView(RequestArgumentMixin, PermissionsMixin, UpdateView): + permission_required = 'scipost.can_invite_fellows' + queryset = RegistrationInvitation.objects.no_response() + form_class = RegistrationInvitationMergeForm + template_name = 'invitations/registrationinvitation_form_merge.html' + success_url = reverse_lazy('invitations:list') + + class RegistrationInvitationsAddCitationView(RequestArgumentMixin, PermissionsMixin, UpdateView): permission_required = 'scipost.can_create_registration_invitations' form_class = RegistrationInvitationAddCitationForm diff --git a/journals/migrations/0027_auto_20180414_2053.py b/journals/migrations/0027_auto_20180414_2053.py new file mode 100644 index 0000000000000000000000000000000000000000..165078a172a03bde9e214369a8d6f04226dcd2ef --- /dev/null +++ b/journals/migrations/0027_auto_20180414_2053.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-04-14 18:53 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0026_auto_20180327_1937'), + ] + + operations = [ + migrations.AlterField( + model_name='publication', + name='doi_label', + field=models.CharField(db_index=True, max_length=200, unique=True, validators=[django.core.validators.RegexValidator('^(SciPostPhysProc|SciPostPhysSel|SciPostPhysLectNotes|SciPostPhys).[0-9]+(.[0-9]+.[0-9]{3,})?$', 'Only valid DOI expressions are allowed (`[a-zA-Z]+.[0-9]+.[0-9]+.[0-9]{3,}` or `[a-zA-Z]+.[0-9]+`)')]), + ), + ] diff --git a/journals/templates/journals/journals.html b/journals/templates/journals/journals.html index a696b5cd0184ce4be0100e0afae9a8c1dbcf23cd..2dad808af08ed6a0f3266302465db2b77a04aab4 100644 --- a/journals/templates/journals/journals.html +++ b/journals/templates/journals/journals.html @@ -69,13 +69,6 @@ </div> <div class="row"> - <div class="col-md-6"> - <h1 class="banner">SciPost Physics Select</h1> - <div class="py-2"> - <p>SciPost Physics Select publishes articles of superlative quality in the field of Physics.</p> - <p>Authors cannot submit directly to this Journal; SPS papers are editorially selected from the most outstanding Submissions to SciPost Physics.</p> - </div> - </div> <div class="col-md-6"> <h1 class="banner"><a href="{% url 'scipost:landing_page' 'SciPostPhysLectNotes' %}">SciPost Physics Lecture Notes</a></h1> <div class="py-2"> diff --git a/journals/views.py b/journals/views.py index a0aec70a3a39d85dec00399cedb61fea24bed4e5..a5fee852fb4deb606f67f47e472de14bb46f6a17 100644 --- a/journals/views.py +++ b/journals/views.py @@ -1152,6 +1152,6 @@ def arxiv_doi_feed(request, doi_label): for publication in publications: feedxml += ('\n<article preprint_id="%s" doi="%s" journal_ref="%s" />' % ( publication.accepted_submission.arxiv_identifier_wo_vn_nr, publication.doi_string, - publication.citation())) + publication.citation)) feedxml += '\n</preprint>' return HttpResponse(feedxml, content_type='text/xml') diff --git a/scipost/static/scipost/images/FJN-logo-long.png b/scipost/static/scipost/images/FJN-logo-long.png new file mode 100755 index 0000000000000000000000000000000000000000..93860c5a2f5c1644e9c4858c398949ec7f0d6390 Binary files /dev/null and b/scipost/static/scipost/images/FJN-logo-long.png differ diff --git a/scipost/templates/scipost/index.html b/scipost/templates/scipost/index.html index 5546719426f347d435abd1155666200f886a4429..58b56563081658effeb20a2c355c1705e1637b41 100644 --- a/scipost/templates/scipost/index.html +++ b/scipost/templates/scipost/index.html @@ -195,6 +195,7 @@ <a href="//www.doaj.org" target="_blank"><img src="{% static 'scipost/images/doaj_logo_200.jpg' %}" width="90" alt="DOAJ logo"></a> <a href="//www.clockss.org" target="_blank"><img src="{% static 'scipost/images/clockss_original_logo_boxed_ai-cropped-90.png' %}" width="80" alt="Clockss logo"></a> <a href="//i4oc.org/" target="_blank"><img width="100" src="{% static 'scipost/images/I4OC.png' %}"></a> + <a href="//freejournals.org" target="_blank"><img width="100" src="{% static 'scipost/images/FJN-logo-long.png' %}"></a> </div> </div> <div class="row"> diff --git a/submissions/models.py b/submissions/models.py index 7b9e7a34859d4161ea1cb4d7386a19a67d3dd3f7..9c3bbcf7b50c7927ca7cf7f65379de024bcd27d0 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -3,6 +3,7 @@ __license__ = "AGPL v3" import datetime +import feedparser from django.contrib.postgres.fields import JSONField from django.contrib.contenttypes.fields import GenericRelation @@ -275,6 +276,32 @@ class Submission(models.Model): ) event.save() + """ + Identify coauthorships from arXiv, using author surname matching. + """ + def flag_coauthorships_arxiv(self, fellows): + coauthorships = {} + if self.metadata and 'entries' in self.metadata: + author_last_names = [] + for author in self.metadata['entries'][0]['authors']: + # Gather author data to do conflict-of-interest queries with + author_last_names.append(author['name'].split()[-1]) + authors_last_names_str = '+OR+'.join(author_last_names) + + for fellow in fellows: + # For each fellow found, so a query with the authors to check for conflicts + search_query = 'au:({fellow}+AND+({authors}))'.format( + fellow=fellow.contributor.user.last_name, + authors=authors_last_names_str) + queryurl = 'https://export.arxiv.org/api/query?search_query={sq}'.format( + sq=search_query) + queryurl += '&sortBy=submittedDate&sortOrder=descending&max_results=5' + queryurl = queryurl.replace(' ', '+') # Fallback for some last names with spaces + queryresults = feedparser.parse(queryurl) + if queryresults.entries: + coauthorships[fellow.contributor.user.last_name] = queryresults.entries + return coauthorships + class SubmissionEvent(SubmissionRelatedObjectMixin, TimeStampedModel): """Private message directly related to a Submission. diff --git a/submissions/templates/partials/submissions/arxiv_queryresult.html b/submissions/templates/partials/submissions/arxiv_queryresult.html index 5f2ad667f742f9c0ea66a2915602b3fde1172c26..b6cb2a65fb2af2b3ad5d0c36306ce6db5f622542 100644 --- a/submissions/templates/partials/submissions/arxiv_queryresult.html +++ b/submissions/templates/partials/submissions/arxiv_queryresult.html @@ -1,10 +1,12 @@ <div class="card-body"> <h3 class="card-title">{{ item.title }}</h3> <div class="card-text"> - {% for author in item.authors %} - {{ author.name }}{% if not forloop.last %},{% endif %} - {% endfor %} - - <a href="{{ item.id }}" target="_blank">{{ item.id }}</a> + <div class="authors mb-2" id="arxiv_authors_{{ id }}" style="display: none;"> + {% for author in item.authors %} + {{ author.name }}{% if not forloop.last %},{% endif %} + {% endfor %} + </div> + <a href="javascript:;" data-toggle="toggle" data-target="#arxiv_authors_{{ id }}">Toggle authors</a> · <a href="{{ item.link }}" target="_blank">{{ item.id }}</a> </div> <p class="card-text text-muted">Published: {{ item.published }}</p> </div> diff --git a/submissions/templates/submissions/admin/editorial_assignment_form.html b/submissions/templates/submissions/admin/editorial_assignment_form.html index df473a80d47ac289a9f7303c1965dff8c4344d05..0761db76ba37f126cea40b4f53ae96df497623da 100644 --- a/submissions/templates/submissions/admin/editorial_assignment_form.html +++ b/submissions/templates/submissions/admin/editorial_assignment_form.html @@ -62,4 +62,38 @@ </div> </div> + +<div class="row"> + <div class="col-12"> + {% if coauthorships %} + <div class="card card-outline-danger"> + <div class="card-body"> + <h3 class="card-title text-danger">The system identified the following potential coauthorships (from arXiv database)</h3> + <p class="card-text text-danger">(only up to 5 most recent shown; if within the last 3 years, referee is disqualified):</p> + </div> + <div class="card-body"> + <ul class="list-group list-group-flush"> + {% for author, entries in coauthorships.items %} + <li class="list-group-item pt-3"> + <div class="card-content"> + <h3>For Fellow: {{ author }}</h3> + </div>{{ value}} + </li> + {% for entry in entries %} + <li class="list-group-item"> + {% include 'partials/submissions/arxiv_queryresult.html' with item=entry id=forloop.counter %} + </li> + {% endfor %} + {% endfor %} + </ul> + </div> + </div> + {% else %} + <h3 class="text-success">The system has not identified any coauthorships (from arXiv database)</h3> + {% endif %} + </div> +</div> + + + {% endblock %} diff --git a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html index 67c157592a55c34617d064ac1bcb1f052b409060..8f9b0781085769edc12de0b7bf0536852e1bd677 100644 --- a/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html +++ b/submissions/templates/submissions/admin/recommendation_prepare_for_voting.html @@ -56,15 +56,15 @@ </div> <div class="card-body"> <ul class="list-group list-group-flush"> - {% for key, value in coauthorships.items %} + {% for author, entries in coauthorships.items %} <li class="list-group-item pt-3"> <div class="card-content"> - <h3>For Fellow {{key}}:</h3> - </div> + <h3>For Fellow: {{ author }}</h3> + </div>{{ value}} </li> - {% for entry in value.entries %} + {% for entry in entries %} <li class="list-group-item"> - {% include 'partials/submissions/arxiv_queryresult.html' with item=entry %} + {% include 'partials/submissions/arxiv_queryresult.html' with item=entry id=forloop.counter %} </li> {% endfor %} {% endfor %} diff --git a/submissions/views.py b/submissions/views.py index 30a3e980fcb3a8d4555bc4c3f1ab35ada22b6882..124890e11000fa6aa98f8f547503150b1dbd75ed 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -451,8 +451,13 @@ def assign_submission(request, arxiv_identifier_w_vn_nr): SubmissionUtils.send_assignment_request_email() messages.success(request, 'Your assignment request has been sent successfully.') return redirect('submissions:pool') + + fellows_with_expertise = submission.fellows.all() + coauthorships = submission.flag_coauthorships_arxiv(fellows_with_expertise) + context = { 'submission_to_assign': submission, + 'coauthorships': coauthorships, 'form': form } return render(request, 'submissions/admin/editorial_assignment_form.html', context) @@ -736,7 +741,7 @@ def select_referee(request, arxiv_identifier_w_vn_nr): sub_auth_boolean_str += '+OR+' + author['name'].split()[-1] sub_auth_boolean_str += ')+AND+' search_str = sub_auth_boolean_str + ref_search_form.cleaned_data['last_name'] + ')' - queryurl = ('http://export.arxiv.org/api/query?search_query=au:%s' + queryurl = ('https://export.arxiv.org/api/query?search_query=au:%s' % search_str + '&sortBy=submittedDate&sortOrder=descending' '&max_results=5') arxivquery = feedparser.parse(queryurl) @@ -1389,11 +1394,6 @@ def prepare_for_voting(request, rec_id): recommendation = get_object_or_404( EICRecommendation.objects.active().filter(submission__in=submissions), id=rec_id) - fellows_with_expertise = recommendation.submission.fellows.filter( - contributor__expertises__contains=[recommendation.submission.subject_area]) - - coauthorships = {} - eligibility_form = VotingEligibilityForm(request.POST or None, instance=recommendation) if eligibility_form.is_valid(): eligibility_form.save() @@ -1406,23 +1406,9 @@ def prepare_for_voting(request, rec_id): return redirect(reverse('submissions:editorial_page', args=[recommendation.submission.arxiv_identifier_w_vn_nr])) else: - # Identify possible co-authorships in last 3 years, disqualifying Fellow from voting: - if recommendation.submission.metadata is not None: - for fellow in fellows_with_expertise: - sub_auth_boolean_str = '((' + (recommendation.submission - .metadata['entries'][0]['authors'][0]['name'] - .split()[-1]) - for author in recommendation.submission.metadata['entries'][0]['authors'][1:]: - sub_auth_boolean_str += '+OR+' + author['name'].split()[-1] - sub_auth_boolean_str += ')+AND+' - search_str = sub_auth_boolean_str + fellow.contributor.user.last_name + ')' - queryurl = ('http://export.arxiv.org/api/query?search_query=au:%s' - % search_str + '&sortBy=submittedDate&sortOrder=descending' - '&max_results=5') - arxivquery = feedparser.parse(queryurl) - queryresults = arxivquery - if queryresults.entries: - coauthorships[fellow.contributor.user.last_name] = queryresults + fellows_with_expertise = recommendation.submission.fellows.filter( + contributor__expertises__contains=[recommendation.submission.subject_area]) + coauthorships = recommendation.submission.flag_coauthorships_arxiv(fellows_with_expertise) context = { 'recommendation': recommendation,