diff --git a/scipost/migrations/0027_citationnotification.py b/scipost/migrations/0027_citationnotification.py new file mode 100644 index 0000000000000000000000000000000000000000..1c0a8e27cf3204f382f2504c667599e81ee42309 --- /dev/null +++ b/scipost/migrations/0027_citationnotification.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-29 09:47 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0028_auto_20161212_1931'), + ('journals', '0006_publication_citedby'), + ('scipost', '0026_draftinvitation'), + ] + + operations = [ + migrations.CreateModel( + name='CitationNotification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_drafted', models.DateTimeField(default=django.utils.timezone.now)), + ('processed', models.BooleanField(default=False)), + ('cited_in_publication', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='journals.Publication')), + ('cited_in_submission', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='submissions.Submission')), + ('contributor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='scipost.Contributor')), + ], + ), + ] diff --git a/scipost/migrations/0028_remove_citationnotification_date_drafted.py b/scipost/migrations/0028_remove_citationnotification_date_drafted.py new file mode 100644 index 0000000000000000000000000000000000000000..79b4fa512e38c172a645cce0cdba293d74a6c31d --- /dev/null +++ b/scipost/migrations/0028_remove_citationnotification_date_drafted.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-29 09:47 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0027_citationnotification'), + ] + + operations = [ + migrations.RemoveField( + model_name='citationnotification', + name='date_drafted', + ), + ] diff --git a/scipost/models.py b/scipost/models.py index bc1395d9aed8ff8bd567928510bda2093a80d1bb..e03f1efce556ce9e357519bc635ef828ccd53714 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -258,7 +258,7 @@ class Contributor(models.Model): 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> @@ -443,6 +443,25 @@ class RegistrationInvitation(models.Model): + ' on ' + self.date_sent.strftime("%Y-%m-%d")) +class CitationNotification(models.Model): + contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE) + cited_in_submission = models.ForeignKey('submissions.Submission', + on_delete=models.CASCADE, + blank=True, null=True) + cited_in_publication = models.ForeignKey('journals.Publication', + on_delete=models.CASCADE, + blank=True, null=True) + processed = models.BooleanField(default=False) + + def __str__(self): + text = str(self.contributor) + ', cited in ' + if self.cited_in_submission: + text += self.cited_in_submission.arxiv_nr_w_vn_nr + elif self.cited_in_publication: + text += self.cited_in_publication.citation() + if self.processed: + text += ' (processed)' + return text AUTHORSHIP_CLAIM_STATUS = ( (1, 'accepted'), diff --git a/scipost/templates/scipost/citation_notifications.html b/scipost/templates/scipost/citation_notifications.html new file mode 100644 index 0000000000000000000000000000000000000000..0cc98b155245deb66f6b2fb13cb78ad5a00d47d7 --- /dev/null +++ b/scipost/templates/scipost/citation_notifications.html @@ -0,0 +1,29 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: citation notifications{% endblock pagetitle %} + +{% block bodysup %} + + +<section> + <div class="flex-greybox"> + <h1>Citation notifications to process</h1> + </div> + + {% if errormessage %} + <h3 style="color: red;">{{ errormessage }}</h3> + {% endif %} + + {% if unprocessed_notifications %} + <ul> + {% for un in unprocessed_notifications %} + <li>{{ un }} <a href="{% url 'scipost:process_citation_notification' cn_id=un.id %}">Process this notification</li> + {% endfor %} + </ul> + {% else %} + <h3>There are no citation notifications to process.</h3> + <p>Return to your <a href="{% url 'scipost:personal_page' %}">personal page</a>.</p> + {% endif %} + + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/edit_draft_reg_inv.html b/scipost/templates/scipost/edit_draft_reg_inv.html new file mode 100644 index 0000000000000000000000000000000000000000..fd3e872cba4410dca215921871304c9a00ec1d7b --- /dev/null +++ b/scipost/templates/scipost/edit_draft_reg_inv.html @@ -0,0 +1,56 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: edit draft reg inv{% endblock pagetitle %} + +{% block bodysup %} + +<script> + $(document).ready(function(){ + + switch ($('select#id_invitation_type').val()) { + case "ci": + $("#div_id_cited_in_submission").show(); + $("#div_id_cited_in_publication").hide(); + break; + case "cp": + $("#div_id_cited_in_submission").hide(); + $("#div_id_cited_in_publication").show(); + break; + default: + $("#div_id_cited_in_submission").hide(); + $("#div_id_cited_in_publication").hide(); + } + + $('select#id_invitation_type').on('change', function() { + switch ($('select#id_invitation_type').val()) { + case "ci": + $("#div_id_cited_in_submission").show(); + $("#div_id_cited_in_publication").hide(); + break; + case "cp": + $("#div_id_cited_in_submission").hide(); + $("#div_id_cited_in_publication").show(); + break; + default: + $("#div_id_cited_in_submission").hide(); + $("#div_id_cited_in_publication").hide(); + } + }); + }); +</script> + +<section> + <div class="flex-greybox"> + <h1>Edit a draft registration invitation</h1> + </div> + {% if errormessage %} + <h3 style="color: red;">{{ errormessage }}</h3> + {% endif %} + <form action="{% url 'scipost:edit_draft_reg_inv' draft_id=draft.id %}" method="post"> + {% csrf_token %} + {% load crispy_forms_tags %} + {% crispy draft_inv_form %} + </form> +</section> + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index 0d218ed6d19afcfec82ddb7941f3d2a09e52d241..d2645698fd4982108e18cd393f6bad8a3bee8bed 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -244,6 +244,12 @@ <li><a href="{% url 'scipost:registration_invitations' %}">Manage Registration Invitations</a></li> {% endif %} </ul> + <h3>Notifications</h3> + <ul> + {% if perms.scipost.can_manage_registration_invitations %} + <li><a href="{% url 'scipost:citation_notifications' %}">Manage citation notifications</a></li> + {% endif %} + </ul> <h3>Email communications</h3> <ul> {% if perms.scipost.can_email_group_members %} diff --git a/scipost/templates/scipost/registration_invitations.html b/scipost/templates/scipost/registration_invitations.html index f74909628e7797f71ad13b1c829c1ec66acdfab7..0b5b8cb50a7d1072c853a8658c2bba0aeb8fabda 100644 --- a/scipost/templates/scipost/registration_invitations.html +++ b/scipost/templates/scipost/registration_invitations.html @@ -9,8 +9,19 @@ <script> $(document).ready(function(){ + switch ($('select#id_invitation_type').val()) { + case "ci": + $("#div_id_cited_in_submission").show(); + $("#div_id_cited_in_publication").hide(); + break; + case "cp": + $("#div_id_cited_in_submission").hide(); + $("#div_id_cited_in_publication").show(); + break; + default: $("#div_id_cited_in_submission").hide(); $("#div_id_cited_in_publication").hide(); + } $('select#id_invitation_type').on('change', function() { switch ($('select#id_invitation_type').val()) { @@ -67,7 +78,12 @@ <td>{{ draft.date_drafted }} </td> <td>{{ draft.invitation_type }}</td> <td>{{ draft.drafted_by.user.last_name }}</td> + <td><a href="{% url 'scipost:edit_draft_reg_inv' draft_id=draft.id %}">Edit</a></td> <td><a href="{% url 'scipost:registration_invitations_from_draft' draft_id=draft.id %}">Process</a></td> + <td><a href="{% url 'scipost:mark_draft_inv_as_processed' draft_id=draft.id %}">Mark as processed</a></td> + {% for ac in draft|associated_contributors %} + <td><a href="{% url 'scipost:map_draft_reg_inv_to_contributor' draft_id=draft.id contributor_id=ac.id %}">Map to {{ ac.user.first_name }} {{ ac.user.last_name }}</a></td> + {% endfor %} </tr> {% endfor %} </table> diff --git a/scipost/templatetags/scipost_extras.py b/scipost/templatetags/scipost_extras.py index 2c7c70788eab4fd90681437b46a96ba4b874a9b9..3a34f87e76bc8559d8943d360f43469a14860f95 100644 --- a/scipost/templatetags/scipost_extras.py +++ b/scipost/templatetags/scipost_extras.py @@ -1,6 +1,8 @@ from django import template from django.contrib.auth.models import Group +from scipost.models import Contributor + register = template.Library() @@ -21,3 +23,9 @@ def sort_by(queryset, order): def is_in_group(user, group_name): group = Group.objects.get(name=group_name) return True if group in user.groups.all() else False + + +@register.filter(name='associated_contributors') +def associated_contributors(draft): + return Contributor.objects.filter( + user__last_name__icontains=draft.last_name) diff --git a/scipost/urls.py b/scipost/urls.py index 0b33f90cb0a0ac5f5fabb52ffea9aa69c5865c3b..8791b63d18bea458175646f1ab92910161f32c95 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -67,6 +67,10 @@ urlpatterns = [ views.registration_invitations, name="registration_invitations"), url(r'^draft_registration_invitation$', views.draft_registration_invitation, name="draft_registration_invitation"), + url(r'^edit_draft_reg_inv/(?P<draft_id>[0-9]+)$', + views.edit_draft_reg_inv, name="edit_draft_reg_inv"), + url(r'^map_draft_reg_inv_to_contributor/(?P<draft_id>[0-9]+)/(?P<contributor_id>[0-9]+)$', + views.map_draft_reg_inv_to_contributor, name="map_draft_reg_inv_to_contributor"), url(r'^registration_invitations_cleanup$', views.registration_invitations_cleanup, name="registration_invitations_cleanup"), @@ -90,6 +94,12 @@ urlpatterns = [ url(r'^accept_invitation_error$', TemplateView.as_view(template_name='scipost/accept_invitation_error.html'), name='accept_invitation_error'), + url(r'^mark_draft_inv_as_processed/(?P<draft_id>[0-9]+)$', + views.mark_draft_inv_as_processed, name='mark_draft_inv_as_processed'), + url(r'^citation_notifications$', + views.citation_notifications, name='citation_notifications'), + url(r'^process_citation_notification/(?P<cn_id>[0-9]+)$', + views.process_citation_notification, name='process_citation_notification'), ## Authentication url(r'^login/$', views.login_view, name='login'), diff --git a/scipost/utils.py b/scipost/utils.py index d263d7ced541b06427f8dde4c08645dbde84a5c2..369f05f861ff3473ff778deede5e24c788181421 100644 --- a/scipost/utils.py +++ b/scipost/utils.py @@ -653,3 +653,63 @@ class Utils(object): # This function is now for all invitation types: emailmessage.send(fail_silently=False) + + + @classmethod + def send_citation_notification_email(cls): + """ + Requires loading the 'notification' attribute. + """ + email_context = Context({}) + email_text = ('Dear ' + title_dict[cls.notification.contributor.title] + + ' ' + cls.notification.contributor.user.last_name) + email_text_html = 'Dear {{ title }} {{ last_name }}' + email_context['title'] = title_dict[cls.notification.contributor.title] + email_context['last_name'] = cls.notification.contributor.user.last_name + email_text += ',\n\n' + email_text_html += ',<br/>' + if cls.notification.cited_in_publication: + email_text += ( + 'Your work has been cited in a paper published by SciPost,' + '\n\n' + cls.notification.cited_in_publication.title + + '\nby ' + cls.notification.cited_in_publication.author_list + + '\n\n(published as ' + cls.notification.cited_in_publication.citation() + + ').\n\n') + email_text_html += ( + '<p>Your work has been cited in a paper published by SciPost,</p>' + '<p>{{ pub_title }}</p> <p>by {{ pub_author_list }}<p/>' + '(published as <a href="https://scipost.org/{{ doi_label }}">{{ citation }}</a>).' + '</p><br/>' + EMAIL_FOOTER) + email_context['pub_title'] = cls.notification.cited_in_publication.title + email_context['pub_author_list'] = cls.notification.cited_in_publication.author_list + email_context['doi_label'] = cls.notification.cited_in_publication.doi_label + email_context['citation'] = cls.notification.cited_in_publication.citation() + html_template = Template(email_text_html) + html_version = html_template.render(email_context) + elif cls.notification.cited_in_submission: + email_text += ( + 'Your work has been cited in a manuscript submitted to SciPost,' + '\n\n' + cls.notification.cited_in_submission.title + + ' by ' + cls.notification.cited_in_submission.author_list + '.\n\n' + 'You might for example consider reporting or ' + 'commenting on the above submission before the refereeing deadline.') + email_text_html += ( + '<p>Your work has been cited in a manuscript submitted to SciPost,</p>' + '<p>{{ sub_title }} <br>by {{ sub_author_list }},</p>' + '<p>which you can find online at the ' + '<a href="https://scipost.org/submission/{{ arxiv_nr_w_vn_nr }}">' + 'submission\'s page</a>.</p>' + '<p>You might for example consider reporting or ' + 'commenting on the above submission before the refereeing deadline.</p>') + email_context['sub_title'] = cls.notification.cited_in_submission.title + email_context['sub_author_list'] = cls.notification.cited_in_submission.author_list + email_context['arxiv_identifier_w_vn_nr'] = cls.notification.cited_in_submission.arxiv_identifier_w_vn_nr + + emailmessage = EmailMultiAlternatives( + 'SciPost: citation notification', email_text, + 'SciPost admin <admin@scipost.org>', + [cls.notification.contributor.user.email], + bcc=['admin@scipost.org'], + reply_to=['admin@scipost.org']) + emailmessage.attach_alternative(html_version, 'text/html') + emailmessage.send(fail_silently=False) diff --git a/scipost/views.py b/scipost/views.py index 6c38ae2a540a927ff67bb215a90b43fd9acb83c5..e88be053ca23985a1009a237867ca8f388871309 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -576,11 +576,58 @@ def draft_registration_invitation(request): return render(request, 'scipost/draft_registration_invitation.html', context) +@permission_required('scipost.can_manage_registration_invitations', return_403=True) +def edit_draft_reg_inv(request, draft_id): + draft = get_object_or_404(DraftInvitation, id=draft_id) + errormessage = '' + if request.method == 'POST': + draft_inv_form = DraftInvitationForm(request.POST) + if draft_inv_form.is_valid(): + draft.title = draft_inv_form.cleaned_data['title'] + draft.first_name = draft_inv_form.cleaned_data['first_name'] + draft.last_name = draft_inv_form.cleaned_data['last_name'] + draft.email = draft_inv_form.cleaned_data['email'] + draft.save() + return redirect(reverse('scipost:registration_invitations')) + else: + errormessage = 'The form is invalidly filled' + else: + draft_inv_form = DraftInvitationForm(instance=draft) + context = {'draft_inv_form': draft_inv_form, + 'draft': draft, + 'errormessage': errormessage,} + return render(request, 'scipost/edit_draft_reg_inv.html', context) + + +@permission_required('scipost.can_manage_registration_invitations', return_403=True) +def map_draft_reg_inv_to_contributor(request, draft_id, contributor_id): + """ + If a draft invitation actually points to an already-registered + Contributor, this method marks the draft invitation as processed + and, if the draft invitation was for a citation type, + creates an instance of CitationNotification. + """ + draft = get_object_or_404(DraftInvitation, id=draft_id) + contributor = get_object_or_404(Contributor, id=contributor_id) + errormessage = '' + draft.processed = True + draft.save() + citation = CitationNotification( + contributor=contributor, + cited_in_submission=draft.cited_in_submission, + cited_in_publication=draft.cited_in_publication, + processed=False) + citation.save() + return redirect(reverse('scipost:registration_invitations')) + + + @permission_required('scipost.can_manage_registration_invitations', return_403=True) def registration_invitations(request, draft_id=None): """ Overview and tools for administrators """ # List invitations sent; send new ones errormessage = '' + associated_contributors = None if request.method == 'POST': # Send invitation from form information reg_inv_form = RegistrationInvitationForm(request.POST) @@ -622,6 +669,8 @@ def registration_invitations(request, draft_id=None): initial = {} if draft_id: draft = get_object_or_404(DraftInvitation, id=draft_id) + associated_contributors = Contributor.objects.filter( + user__last_name__icontains=draft.last_name) initial = {'title': draft.title, 'first_name': draft.first_name, 'last_name': draft.last_name, @@ -687,6 +736,7 @@ def registration_invitations(request, draft_id=None): 'decl_reg_inv': decl_reg_inv, 'names_reg_contributors': names_reg_contributors, 'existing_drafts': existing_drafts, + 'associated_contributors': associated_contributors, } return render(request, 'scipost/registration_invitations.html', context) @@ -768,6 +818,32 @@ def mark_reg_inv_as_declined(request, invitation_id): return redirect(reverse('scipost:registration_invitations')) +@permission_required('scipost.can_manage_registration_invitations', return_403=True) +def citation_notifications(request): + unprocessed_notifications = CitationNotification.objects.filter( + processed=False).order_by('contributor__user__last_name') + context = {'unprocessed_notifications': unprocessed_notifications,} + return render(request, 'scipost/citation_notifications.html', context) + + +@permission_required('scipost.can_manage_registration_invitations', return_403=True) +def process_citation_notification(request, cn_id): + notification = get_object_or_404(CitationNotification, id=cn_id) + notification.processed=True + notification.save() + Utils.load({'notification': notification}) + Utils.send_citation_notification_email() + return redirect(reverse('scipost:citation_notifications')) + + +@permission_required('scipost.can_manage_registration_invitations', return_403=True) +def mark_draft_inv_as_processed(request, draft_id): + draft = get_object_or_404(DraftInvitation, id=draft_id) + draft.processed = True + draft.save() + return redirect(reverse('scipost:registration_invitations')) + + def login_view(request): redirect_to = request.POST.get('next', request.GET.get('next', reverse('scipost:personal_page')))