diff --git a/invitations/admin.py b/invitations/admin.py index 1aaf18857234d0c5917d1297687ebcf0df990a60..a2012f98edee74417477674d8bf2c17a310ec307 100644 --- a/invitations/admin.py +++ b/invitations/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models import RegistrationInvitation +from .models import RegistrationInvitation, CitationNotification class RegistrationInvitationAdmin(admin.ModelAdmin): @@ -11,3 +11,14 @@ class RegistrationInvitationAdmin(admin.ModelAdmin): 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'] + list_display = ['__str__', 'created_by', 'date_sent', 'processed'] + list_filter = ['processed'] + + +admin.site.register(CitationNotification, CitationNotificationAdmin) diff --git a/invitations/constants.py b/invitations/constants.py index b1f0598fa1a9815846075f7e5e33714b0c421df5..9de09698378d2332d28ad699f7fdb06c5257be46 100644 --- a/invitations/constants.py +++ b/invitations/constants.py @@ -1,8 +1,9 @@ -STATUS_DRAFT, STATUS_SENT = ('draft', 'sent') +STATUS_DRAFT, STATUS_SENT, STATUS_SENT_AND_EDITED = ('draft', 'sent', 'edited') STATUS_DECLINED, STATUS_REGISTERED = ('declined', 'register') REGISTATION_INVITATION_STATUSES = ( (STATUS_DRAFT, 'Draft'), (STATUS_SENT, 'Sent'), + (STATUS_SENT_AND_EDITED, 'Sent and edited'), (STATUS_DECLINED, 'Declined'), (STATUS_REGISTERED, 'Registered'), ) diff --git a/invitations/forms.py b/invitations/forms.py index ee5a24a7df9ab474bd3d30f2fa515d8fb720e962..a6afd6c4d66495e8db44ab6e9b99fcc9cfdcf9a8 100644 --- a/invitations/forms.py +++ b/invitations/forms.py @@ -1,13 +1,130 @@ from django import forms +from django.contrib import messages -from .models import RegistrationInvitation +from journals.models import Publication +from scipost.models import Contributor +from submissions.models import Submission -from ajax_select.fields import AutoCompleteSelectMultipleField +from . import constants +from .models import RegistrationInvitation, CitationNotification +from .utils import Utils +from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultipleField -class RegistrationInvitationForm(forms.ModelForm): - cited_in_submission = AutoCompleteSelectMultipleField('submissions_lookup', required=False) - cited_in_publication = AutoCompleteSelectMultipleField('publication_lookup', required=False) + +class AcceptRequestMixin: + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request') + super().__init__(*args, **kwargs) + + +class RegistrationInvitationFilterForm(forms.Form): + last_name = forms.CharField() + + def search(self, qs): + last_name = self.cleaned_data.get('last_name') + return qs.filter(last_name__icontains=last_name) + + +class ContributorSearchForm(forms.Form): + last_name = forms.CharField() + + def search(self): + last_name = self.cleaned_data.get('last_name') + + if last_name: + contributors = Contributor.objects.filter(user__last_name__icontains=last_name) + invitations = RegistrationInvitation.objects.filter(last_name__icontains=last_name) + return contributors, invitations + return Contributor.objects.none(), RegistrationInvitation.objects.none() + + +class CitationNotificationForm(AcceptRequestMixin, forms.ModelForm): + submission = AutoCompleteSelectField('submissions_lookup', required=False) + publication = AutoCompleteSelectField('publication_lookup', required=False) + + class Meta: + model = CitationNotification + fields = ( + 'contributor', + 'submission', + 'publication') + + def __init__(self, *args, **kwargs): + contributors = kwargs.pop('contributors') + super().__init__(*args, **kwargs) + if contributors: + self.fields['contributor'].queryset = contributors + self.fields['contributor'].empty_label = None + else: + self.fields['contributor'].queryset = Contributor.objects.none() + + def save(self, *args, **kwargs): + if not hasattr(self.instance, 'created_by'): + self.instance.created_by = self.request.user + return super().save(*args, **kwargs) + + +class CitationNotificationProcessForm(AcceptRequestMixin, forms.ModelForm): + class Meta: + model = CitationNotification + fields = () + + def get_all_notifications(self): + return self.instance.related_notifications().unprocessed() + + def save(self, *args, **kwargs): + if kwargs.get('commit', True): + self.get_all_notifications().update(processed=True) + + contributor = self.get_all_notifications().filter(contributor__isnull=False)[0] + send_mail = (contributor and contributor.accepts_SciPost_emails) or not contributor + if send_mail: + Utils.load({'notifications': self.get_all_notifications()}) + Utils.citation_notifications_email() + return super().save(*args, **kwargs) + + +class RegistrationInvitationAddCitationForm(AcceptRequestMixin, forms.ModelForm): + cited_in_submissions = AutoCompleteSelectMultipleField('submissions_lookup', required=False) + cited_in_publications = AutoCompleteSelectMultipleField('publication_lookup', required=False) + + class Meta: + model = RegistrationInvitation + fields = () + + def save(self, *args, **kwargs): + if kwargs.get('commit', True): + updated = 0 + # Save the Submission notifications + submissions = Submission.objects.filter( + id__in=self.cleaned_data['cited_in_submissions']) + for submission in submissions: + __, _updated = CitationNotification.objects.get_or_create( + invitation=self.instance, + submission=submission, + defaults={'created_by': self.request.user}) + updated += 1 if _updated else 0 + + # Save the Publication notifications + publications = Publication.objects.filter( + id__in=self.cleaned_data['cited_in_publications']) + for publication in publications: + __, _updated = CitationNotification.objects.get_or_create( + invitation=self.instance, + publication=publication, + defaults={'created_by': self.request.user}) + updated += 1 if _updated else 0 + if updated > 0: + self.instance.status = constants.STATUS_SENT_AND_EDITED + self.instance.save() + messages.success(self.request, '{} Citation Notification(s) added.'.format(updated)) + return self.instance + + +class RegistrationInvitationForm(AcceptRequestMixin, forms.ModelForm): + cited_in_submissions = AutoCompleteSelectMultipleField('submissions_lookup', required=False) + cited_in_publications = AutoCompleteSelectMultipleField('publication_lookup', required=False) class Meta: model = RegistrationInvitation @@ -17,12 +134,20 @@ class RegistrationInvitationForm(forms.ModelForm): 'last_name', 'email', 'message_style', - 'personal_message', - 'cited_in_submission', - 'cited_in_publication') + 'personal_message') def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request') + # Find Submissions/Publications related to the invitation and fill the autocomplete fields + initial = kwargs.get('initial', {}) + invitation = kwargs.get('instance', None) + if invitation: + submission_ids = invitation.citation_notifications.for_submissions().values_list( + 'submission_id', flat=True) + publication_ids = invitation.citation_notifications.for_publications().values_list( + 'publication_id', flat=True) + initial['cited_in_submissions'] = Submission.objects.filter(id__in=submission_ids) + initial['cited_in_publications'] = Publication.objects.filter(id__in=publication_ids) + kwargs['initial'] = initial super().__init__(*args, **kwargs) if not self.request.user.has_perm('scipost.can_manage_registration_invitations'): del self.fields['message_style'] @@ -31,18 +156,76 @@ class RegistrationInvitationForm(forms.ModelForm): def save(self, *args, **kwargs): if not hasattr(self.instance, 'created_by'): self.instance.created_by = self.request.user + invitation = super().save(*args, **kwargs) + if kwargs.get('commit', True): + # Save the Submission notifications + submissions = Submission.objects.filter( + id__in=self.cleaned_data['cited_in_submissions']) + for submission in submissions: + CitationNotification.objects.get_or_create( + invitation=self.instance, + submission=submission, + defaults={ + 'created_by': self.instance.created_by + }) + + # Save the Publication notifications + publications = Publication.objects.filter( + id__in=self.cleaned_data['cited_in_publications']) + for publication in publications: + CitationNotification.objects.get_or_create( + invitation=self.instance, + publication=publication, + defaults={ + 'created_by': self.instance.created_by + }) + return invitation + + +class RegistrationInvitationReminderForm(AcceptRequestMixin, forms.ModelForm): + class Meta: + model = RegistrationInvitation + fields = () + + def save(self, *args, **kwargs): + if kwargs.get('commit', True): + self.instance.mail_sent() return super().save(*args, **kwargs) -class RegistrationInvitationReminderForm(forms.ModelForm): +class RegistrationInvitationMapToContributorForm(AcceptRequestMixin, forms.ModelForm): + contributor = None + class Meta: model = RegistrationInvitation fields = () - def __init__(self, *args, **kwargs): - self.request = kwargs.pop('request') - super().__init__(*args, **kwargs) + def clean(self, *args, **kwargs): + try: + self.contributor = Contributor.objects.get( + id=self.request.resolver_match.kwargs['contributor_id']) + except Contributor.DoesNotExist: + self.add_error(None, 'Contributor does not exist.') + return {} + + def get_contributor(self): + if not self.contributor: + self.clean() + return self.contributor def save(self, *args, **kwargs): - self.mail_sent() - return super().save(*args, **kwargs) + if kwargs.get('commit', True): + self.instance.citation_notifications.update(contributor=self.contributor) + self.instance.delete() + return self.instance + + +class RegistrationInvitationMarkForm(AcceptRequestMixin, forms.ModelForm): + class Meta: + model = RegistrationInvitation + fields = () + + def save(self, *args, **kwargs): + if kwargs.get('commit', True): + self.instance.mail_sent() + return self.instance diff --git a/invitations/managers.py b/invitations/managers.py index 7954565fa5953e74a8ed36e4faa42d870ba27c15..0f931c825b6bb142d9f1964b367ce0f6046d3040 100644 --- a/invitations/managers.py +++ b/invitations/managers.py @@ -16,8 +16,27 @@ class RegistrationInvitationQuerySet(models.QuerySet): def drafts(self): return self.filter(status=constants.STATUS_DRAFT) - def pending_response(self): - return self.filter(status=constants.STATUS_SENT) + def sent(self): + return self.filter(status__in=[constants.STATUS_SENT, constants.STATUS_SENT_AND_EDITED]) + + def no_response(self): + return self.filter(status__in=[constants.STATUS_SENT, + constants.STATUS_DRAFT, + constants.STATUS_SENT_AND_EDITED]) def invited_by(self, user): return self.filter(invited_by=user) + + +class CitationNotificationQuerySet(models.QuerySet): + def for_submissions(self): + return self.filter(submission__isnull=False) + + def for_publications(self): + return self.filter(publication__isnull=False) + + def unprocessed(self): + return self.filter(processed=False) + + def processed(self): + return self.filter(processed=False) diff --git a/invitations/migrations/0007_auto_20180218_1200.py b/invitations/migrations/0007_auto_20180218_1200.py new file mode 100644 index 0000000000000000000000000000000000000000..223d8fbb57806086568323a1fe6e2f72f653ddce --- /dev/null +++ b/invitations/migrations/0007_auto_20180218_1200.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-18 11:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journals', '0013_auto_20180216_0850'), + ('submissions', '0008_auto_20180127_2208'), + ('invitations', '0006_auto_20180217_1600'), + ] + + operations = [ + migrations.RemoveField( + model_name='registrationinvitation', + name='cited_in_publication', + ), + migrations.RemoveField( + model_name='registrationinvitation', + name='cited_in_submission', + ), + migrations.AddField( + model_name='registrationinvitation', + name='cited_in_publications', + field=models.ManyToManyField(blank=True, related_name='_registrationinvitation_cited_in_publications_+', to='journals.Publication'), + ), + migrations.AddField( + model_name='registrationinvitation', + name='cited_in_submissions', + field=models.ManyToManyField(blank=True, related_name='_registrationinvitation_cited_in_submissions_+', to='submissions.Submission'), + ), + ] diff --git a/invitations/migrations/0008_auto_20180218_1305.py b/invitations/migrations/0008_auto_20180218_1305.py new file mode 100644 index 0000000000000000000000000000000000000000..d74f26c02b7c9411230ea18f421b156c4bfb4104 --- /dev/null +++ b/invitations/migrations/0008_auto_20180218_1305.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-18 12:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0008_auto_20180127_2208'), + ('journals', '0013_auto_20180216_0850'), + ('scipost', '0004_auto_20180212_1932'), + ('invitations', '0007_auto_20180218_1200'), + ] + + operations = [ + migrations.CreateModel( + name='CitationNotification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('processed', models.BooleanField(default=False)), + ('cited_in_publication', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='journals.Publication')), + ('cited_in_submission', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='submissions.Submission')), + ('contributor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='scipost.Contributor')), + ], + options={ + 'default_related_name': 'citation_notifications', + }, + ), + migrations.RemoveField( + model_name='registrationinvitation', + name='cited_in_publications', + ), + migrations.RemoveField( + model_name='registrationinvitation', + name='cited_in_submissions', + ), + migrations.AddField( + 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/migrations/0009_auto_20180218_1556.py b/invitations/migrations/0009_auto_20180218_1556.py new file mode 100644 index 0000000000000000000000000000000000000000..a755f03d264593dc8191ba51b3293aa5c196dc3d --- /dev/null +++ b/invitations/migrations/0009_auto_20180218_1556.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-18 14:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0005_auto_20180218_1556'), + ('submissions', '0008_auto_20180127_2208'), + ('journals', '0013_auto_20180216_0850'), + ('invitations', '0008_auto_20180218_1305'), + ] + + operations = [ + migrations.RenameField( + model_name='citationnotification', + old_name='cited_in_publication', + new_name='publication', + ), + migrations.RenameField( + model_name='citationnotification', + old_name='cited_in_submission', + new_name='submission', + ), + migrations.AlterUniqueTogether( + name='citationnotification', + unique_together=set([('contributor', 'publication'), ('invitation', 'publication'), ('invitation', 'submission'), ('contributor', 'submission')]), + ), + ] diff --git a/invitations/migrations/0010_auto_20180218_1613.py b/invitations/migrations/0010_auto_20180218_1613.py new file mode 100644 index 0000000000000000000000000000000000000000..b002ed903b52c1e9422bbb2f04404376c1c2a665 --- /dev/null +++ b/invitations/migrations/0010_auto_20180218_1613.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-18 15:13 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('invitations', '0009_auto_20180218_1556'), + ] + + operations = [ + migrations.AddField( + model_name='citationnotification', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='citationnotification', + name='created_by', + field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, related_name='notifications_created', to=settings.AUTH_USER_MODEL), + preserve_default=False, + ), + migrations.AddField( + model_name='citationnotification', + name='date_sent', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='citationnotification', + name='modified', + field=models.DateTimeField(auto_now=True), + ), + migrations.AlterField( + model_name='citationnotification', + name='invitation', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='citation_notifications', to='invitations.RegistrationInvitation'), + ), + ] diff --git a/invitations/mixins.py b/invitations/mixins.py index 1e996e16d24c1119f1bf651937749f58c40496be..4b23a2f302e3f4ad7ff65cf4439b59844b79c33b 100644 --- a/invitations/mixins.py +++ b/invitations/mixins.py @@ -2,22 +2,40 @@ from django.db import transaction from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin +from .utils import Utils -class RegistrationInvitationFormMixin(LoginRequiredMixin, PermissionRequiredMixin): + +class RequestArgumentMixin: + """ + Use the WSGIRequest as an argument in the form. + """ def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['request'] = self.request return kwargs + +class PermissionsMixin(LoginRequiredMixin, PermissionRequiredMixin): + pass + + +class SaveAndSendFormMixin: + """ + Use the Save or Save and Send option to send the mail out after form is valid. + """ @transaction.atomic def form_valid(self, form): response = super().form_valid(form) send_mail = self.request.POST.get('save', '') == 'save_and_send' if send_mail: + # Confirm permissions for user send_mail = self.request.user.has_perm('scipost.can_manage_registration_invitations') + model_name = self.object._meta.verbose_name if send_mail: self.object.mail_sent() - messages.success(self.request, 'Registration Invitation updated and sent') + Utils.load({model_name: self.object}) + getattr(Utils, self.utils_email_method)() + messages.success(self.request, '{} updated and sent'.format(model_name)) else: - messages.success(self.request, 'Registration Invitation updated') + messages.success(self.request, '{} updated'.format(model_name)) return response diff --git a/invitations/models.py b/invitations/models.py index 1c8dcf464256fbc3fbd23a72434f8d56ac3c6cce..cf8bc8cfdd390092821bec51ca39d0c3bf40761e 100644 --- a/invitations/models.py +++ b/invitations/models.py @@ -3,12 +3,12 @@ import hashlib import random import string -from django.db import models +from django.db import models, IntegrityError from django.conf import settings from django.utils import timezone from . import constants -from .managers import RegistrationInvitationQuerySet +from .managers import RegistrationInvitationQuerySet, CitationNotificationQuerySet from scipost.constants import TITLE_CHOICES @@ -35,10 +35,10 @@ class RegistrationInvitation(models.Model): # Related to objects invitation_type = models.CharField(max_length=2, choices=constants.INVITATION_TYPE, default=constants.INVITATION_CONTRIBUTOR) - cited_in_submission = models.ManyToManyField('submissions.Submission', - blank=True, related_name='+') - cited_in_publication = models.ManyToManyField('journals.Publication', - blank=True, related_name='+') + # cited_in_submissions = models.ManyToManyField('submissions.Submission', + # blank=True, related_name='+') + # cited_in_publications = models.ManyToManyField('journals.Publication', + # blank=True, related_name='+') # Response keys invitation_key = models.CharField(max_length=40, unique=True) @@ -88,5 +88,77 @@ class RegistrationInvitation(models.Model): self.date_sent_first = timezone.now() self.date_sent_last = timezone.now() self.invited_by = user or self.created_by - self.times_sent = self.times_sent + 1 + self.times_sent += 1 + self.citation_notifications.update(processed=True) self.save() + + +class CitationNotification(models.Model): + invitation = models.ForeignKey('invitations.RegistrationInvitation', + on_delete=models.SET_NULL, + null=True, blank=True) + contributor = models.ForeignKey('scipost.Contributor', + on_delete=models.CASCADE, + null=True, blank=True, + related_name='+') + + # Content + submission = models.ForeignKey('submissions.Submission', null=True, blank=True, + related_name='+') + publication = models.ForeignKey('journals.Publication', null=True, blank=True, + related_name='+') + processed = models.BooleanField(default=False) + + # Meta info + created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='notifications_created') + date_sent = models.DateTimeField(null=True, blank=True) + created = models.DateTimeField(auto_now_add=True) + modified = models.DateTimeField(auto_now=True) + + objects = CitationNotificationQuerySet.as_manager() + + class Meta: + default_related_name = 'citation_notifications' + unique_together = ( + ('invitation', 'submission'), + ('invitation', 'publication'), + ('contributor', 'submission'), + ('contributor', 'publication'), + ) + + def __str__(self): + _str = 'Citation for ' + if self.invitation: + _str += ' Invitation ({} {})'.format( + self.invitation.first_name, + self.invitation.last_name, + ) + elif self.contributor: + _str += ' Contributor ({})'.format(self.contributor) + + _str += ' on ' + if self.submission: + _str += 'Submission ({})'.format(self.submission.arxiv_identifier_w_vn_nr) + elif self.publication: + _str += 'Publication ({})'.format(self.publication.doi_label) + return _str + + def save(self, *args, **kwargs): + if not self.submission and not self.publication: + raise IntegrityError(('CitationNotification needs to be related to either a ' + 'Submission or Publication object.')) + return super().save(*args, **kwargs) + + def mail_sent(self): + """ + Update instance fields as if a new citation notification mail has been sent out. + """ + self.processed = True + if not self.date_sent: + # Don't overwrite by accident... + self.date_sent = timezone.now() + self.save() + + def related_notifications(self): + return CitationNotification.objects.filter( + models.Q(contributor=self.contributor) | models.Q(invitation=self.invitation)) diff --git a/invitations/templates/invitations/citationnotification_form.html b/invitations/templates/invitations/citationnotification_form.html new file mode 100644 index 0000000000000000000000000000000000000000..49d8c3eed57363ad33725729142db774d0a808fe --- /dev/null +++ b/invitations/templates/invitations/citationnotification_form.html @@ -0,0 +1,39 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Process Citation Notification{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <a href="{% url 'invitations:citation_notification_list' %}" class="breadcrumb-item">Citation Notifications</a> + <span class="breadcrumb-item">Process</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Process Citation Notification</h1> + + <h3>All related unprocessed Citation Notifications</h3> + {% for related_notification in form.get_all_notifications %} + {% include 'partials/invitations/citationnotification_summary.html' with notification=related_notification %} + <br> + {% endfor %} + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" name="save" value="Process all notifications"> + </form> + + </div> +</div> + +{% endblock %} diff --git a/invitations/templates/invitations/citationnotification_list.html b/invitations/templates/invitations/citationnotification_list.html new file mode 100644 index 0000000000000000000000000000000000000000..c24a4abb6a3851fd9883ff60f8592c481a1d8da6 --- /dev/null +++ b/invitations/templates/invitations/citationnotification_list.html @@ -0,0 +1,25 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% load bootstrap %} + +{% block pagetitle %}: Unprocessed Citation Notifications{% endblock pagetitle %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Unprocessed Citation Notifications</span> +{% endblock %} + +{% block content %} + +<h1 class="highlight">Unprocessed Citation Notifications</h1> +<a href="{% url 'invitations:list' %}">Back to Registration Invitations</a> + +<div class="row"> + <div class="col-12"> + <br> + {% include 'partials/invitations/citationnotification_table.html' with notifications=object_list %} + </div> +</div> + +{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_confirm_delete.html b/invitations/templates/invitations/registrationinvitation_confirm_delete.html index 989547dc85e2604a5db881a6f862d914fb976279..c65958c8547340e2b5dc9f79d564c029eb5e5166 100644 --- a/invitations/templates/invitations/registrationinvitation_confirm_delete.html +++ b/invitations/templates/invitations/registrationinvitation_confirm_delete.html @@ -14,12 +14,7 @@ <div class="col-12"> <h1 class="highlight">Delete Registration Invitation {{ object.id }}</h1> <p>Are you sure you want to delete the Registration Invitation?</p> - - <p> - Name: <strong>{{ object.first_name }} {{ object.last_name }}</strong> - <br> - Email: <strong>{{ object.email }}</strong> - </p> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} <form method="post"> {% csrf_token %} diff --git a/invitations/templates/invitations/registrationinvitation_form.html b/invitations/templates/invitations/registrationinvitation_form.html index a552c0d0b9658a6943647e24d1b37059c2947bc2..878ad6087d05b52a539f6ff611d3958c1d5b61b5 100644 --- a/invitations/templates/invitations/registrationinvitation_form.html +++ b/invitations/templates/invitations/registrationinvitation_form.html @@ -1,6 +1,6 @@ {% extends 'scipost/_personal_page_base.html' %} -{% block pagetitle %}: {% if object %}Edit{% else %}New{% endif %} Registration Invitation{% endblock pagetitle %} +{% block pagetitle %}: Edit Registration Invitation{% endblock pagetitle %} {% load scipost_extras %} {% load bootstrap %} @@ -8,28 +8,24 @@ {% block breadcrumb_items %} {{block.super}} <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> - <span class="breadcrumb-item">{% if object %}Edit{% else %}New{% endif %}</span> + <span class="breadcrumb-item">Edit</span> {% endblock %} {% block content %} <div class="row"> <div class="col-12"> - <h1 class="highlight">{% if object %}Registration Invitation {{ object.id }}{% else %}New Registration Invitation{% endif %}</h1> - </div> -</div> + <h1 class="highlight">Registration Invitation {{ object.id }}</h1> -<div class="row"> - <div class="col-12"> <form method="post"> {% csrf_token %} {{ form|bootstrap }} - <button type="submit" class="btn btn-primary" name="save" value="save">{% if object %}Update{% else %}Create{% endif %}</button> + <button type="submit" class="btn btn-primary" name="save" value="save">Save</button> + <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_create">Save and create new</button> {% if perms.scipost.can_manage_registration_invitations %} - <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_send">{% if object %}Update{% else %}Create{% endif %} and send</button> + <button type="submit" class="ml-2 btn btn-secondary" name="save" value="save_and_send">Save and send mail</button> {% endif %} </form> - </div> </div> diff --git a/invitations/templates/invitations/registrationinvitation_form_add_citation.html b/invitations/templates/invitations/registrationinvitation_form_add_citation.html new file mode 100644 index 0000000000000000000000000000000000000000..af960d23a5b97ac8823e1b3d2b7b169d71296ccb --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_add_citation.html @@ -0,0 +1,43 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Add Citation Notification{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Add Citation Notification</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Add Citation Notification</h1> + <h3>Registration Invitation</h3> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + </div> +</div> + +<hr class="divider"> + +<div class="row"> + <div class="col-12"> + <h3>Submission or Publication to add to the Registration Invitation</h3> + <br> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Add"> + </form> + + </div> +</div> + +{% endblock %} + +{% block footer_script %} + {{ block.super }} + {{ form.media }} +{% endblock footer_script %} diff --git a/invitations/templates/invitations/registrationinvitation_form_add_new.html b/invitations/templates/invitations/registrationinvitation_form_add_new.html new file mode 100644 index 0000000000000000000000000000000000000000..b8e3129f0a33b705f2a5b24e6902f4713f8476f1 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_add_new.html @@ -0,0 +1,81 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: New 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">New</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">New Registration Invitation</h1> + + {% if contributor_search_form %} + <h3 class="mb-1">Search for existing Contributor</h3> + <form method="get"> + {{ contributor_search_form|bootstrap }} + <input type="submit" class="btn btn-primary" value="Search"> + {% if contributor_search_form.is_bound %} + <a href="{% url 'invitations:new' %}" class="ml-2 btn btn-link">Cancel search</a> + {% endif %} + </form> + + <hr class="divider"> + {% if contributor_search_form.is_bound %} + {% if suggested_invitations %} + <h3>Registration Invitations found</h3> + <ul class="mb-2"> + {% for inv in suggested_invitations %} + <li><a href="{% url 'invitations:add_citation' inv.id %}">Use Registration Invitation for {{ inv.first_name }} {{ inv.last_name }}</a></li> + {% endfor %} + </ul> + {% endif %} + + <h3>Citation Notification</h3> + <br> + {% else %} + <h3 class="mb-1">...or write a new Registration Invitation</h3> + {% endif %} + + {% endif %} + + {% if contributor_search_form.is_bound %} + <form method="post"> + {% csrf_token %} + {{ citation_form|bootstrap }} + <button type="submit" class="btn btn-primary" name="save" value="save">Save</button> + <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_create">Save and create new</button> + {% if perms.scipost.can_manage_registration_invitations %} + <button type="submit" class="ml-2 btn btn-secondary" name="save" value="save_and_send">Save and send mail</button> + {% endif %} + </form> + + <br> + <a href="{% url 'invitations:new' %}">Cancel search here</a> to write a new Registration Invitation. + {% else %} + <form method="post"> + {% csrf_token %} + {{ invitation_form|bootstrap }} + <button type="submit" class="btn btn-primary" name="save" value="save">Save</button> + <button type="submit" class="ml-2 btn btn-primary" name="save" value="save_and_create">Save and create new</button> + {% if perms.scipost.can_manage_registration_invitations %} + <button type="submit" class="ml-2 btn btn-secondary" name="save" value="save_and_send">Save and send mail</button> + {% endif %} + </form> + {% endif %} + </div> +</div> + +{% endblock %} + +{% block footer_script %} + {{ block.super }} + {{ invitation_form.media }} +{% endblock footer_script %} diff --git a/invitations/templates/invitations/registrationinvitation_form_map_to_contributor.html b/invitations/templates/invitations/registrationinvitation_form_map_to_contributor.html new file mode 100644 index 0000000000000000000000000000000000000000..46b8e5fa11019753e665d7c263a9d52b0ce52824 --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_map_to_contributor.html @@ -0,0 +1,38 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Map Registration Invitation{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Map to Contributor</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Map Registration Invitation to Contributor</h1> + <h3>Registration Invitation</h3> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <h3>Map to Contributor</h3> + {{ form.get_contributor.get_title_display }} {{ form.get_contributor.user.first_name }} {{ form.get_contributor.user.last_name }} + <br> + <br> + <input type="submit" class="btn btn-primary" name="save" value="Map to Contributor"> + </form> + + </div> +</div> + +{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_form_mark_as.html b/invitations/templates/invitations/registrationinvitation_form_mark_as.html new file mode 100644 index 0000000000000000000000000000000000000000..e288ceadd2fa9298764caa9efba8bf38a90421bf --- /dev/null +++ b/invitations/templates/invitations/registrationinvitation_form_mark_as.html @@ -0,0 +1,33 @@ +{% extends 'scipost/_personal_page_base.html' %} + +{% block pagetitle %}: Mark Registration Invitation {{ request.resolver_match.kwargs.label }}{% endblock pagetitle %} + +{% load bootstrap %} + +{% block breadcrumb_items %} + {{block.super}} + <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> + <span class="breadcrumb-item">Mark {{ request.resolver_match.kwargs.label }}</span> +{% endblock %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Mark Registration Invitation {{ request.resolver_match.kwargs.label }}</h1> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} + </div> +</div> + +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + <button type="submit" class="btn btn-primary" name="save" value="save">Mark {{ request.resolver_match.kwargs.label }}</button> + </form> + + </div> +</div> + +{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_list.html b/invitations/templates/invitations/registrationinvitation_list.html index 1b4be34a68c04c09eb6c02705bdba37771099c03..9ef7afeeaa17c0fda2f5ba2d806ae1866e807c97 100644 --- a/invitations/templates/invitations/registrationinvitation_list.html +++ b/invitations/templates/invitations/registrationinvitation_list.html @@ -1,5 +1,7 @@ {% extends 'scipost/_personal_page_base.html' %} +{% load bootstrap %} + {% block pagetitle %}: Registration Invitations{% endblock pagetitle %} {% block breadcrumb_items %} @@ -12,35 +14,47 @@ <h1 class="highlight">Registration Invitations</h1> <div class="row"> - <div class="col-md-8"> + <div class="col-md-6"> <h3>Actions</h3> <ul class="mb-0"> - {% if perms.scipost.can_manage_registration_invitations %} - <li><a href="{% url 'invitations:cleanup' %}">Perform a cleanup</a></li> - <li><a href="{% url 'invitations:list_pending_invitations' %}">Show pending invitations</a></li> - {% endif %} {% if perms.scipost.can_create_registration_invitations %} <li><a href="{% url 'invitations:new' %}">Create a new invitation</a></li> {% endif %} + {% if perms.scipost.can_manage_registration_invitations %} + <li><a href="{% url 'invitations:cleanup' %}">Perform a cleanup</a></li> + <li><a href="{% url 'invitations:citation_notification_list' %}">List unprocessed Citation Notifications</a></li> + {% endif %} </ul> </div> - <div class="col-md-4 text-right"> + <div class="col-md-6 text-md-right"> <h3>Quick stats</h3> Number in draft: {{ count_in_draft }}<br> Number pending response: {{ count_pending }} + + {% if perms.scipost.can_create_registration_invitations %} + <br><br> + <a href="{% url 'invitations:new' %}" class="btn btn-primary">Create a new invitation</a> + {% endif %} </div> </div> <div class="row"> <div class="col-12"> - <h2 class="highlight">Invitations in draft</h2> - {% if perms.scipost.can_manage_registration_invitations %} - <a href="{% url 'invitations:list_pending_invitations' %}">Show pending invitations</a> - <br> - {% endif %} - + <h2 class="highlight">Registration Invitations</h2> + </div> + <div class="col-md-6"> + <form method="get"> + {{ search_form|bootstrap }} + <input class="btn btn-primary" type="submit" value="Filter"> + <a href="{% url 'invitations:list' %}" class="btn btn-link">Reset filter</a> + </form> + </div> + <div class="col-12"> <br> {% include 'partials/invitations/registrationinvitation_table.html' with invitations=object_list %} + {% if search_form.is_bound %} + <a href="{% url 'invitations:list' %}" class="btn btn-link">Reset filter</a> + {% endif %} </div> </div> diff --git a/invitations/templates/invitations/registrationinvitation_pending_list.html b/invitations/templates/invitations/registrationinvitation_pending_list.html deleted file mode 100644 index 62a73082373777afc5c31a0095cc5b2738c44a58..0000000000000000000000000000000000000000 --- a/invitations/templates/invitations/registrationinvitation_pending_list.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'scipost/_personal_page_base.html' %} - -{% block pagetitle %}: Registration Invitations{% endblock pagetitle %} - -{% block breadcrumb_items %} - {{ block.super }} - <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> - <span class="breadcrumb-item">Pending response</span> -{% endblock %} - -{% block content %} - -<h1 class="highlight">Registration Invitations</h1> -<div class="row"> - <div class="col-md-8"> - <h3>Actions</h3> - <ul class="mb-0"> - {% if perms.scipost.can_manage_registration_invitations %} - <li><a href="{% url 'invitations:cleanup' %}">Perform a cleanup</a></li> - <li><a href="{% url 'invitations:list' %}">Show drafted invitations</a></li> - {% endif %} - {% if perms.scipost.can_create_registration_invitations %} - <li><a href="{% url 'invitations:new' %}">Create a new invitation</a></li> - {% endif %} - </ul> - </div> - <div class="col-md-4 text-right"> - <h3>Quick stats</h3> - Number in draft: {{ count_in_draft }}<br> - Number pending response: {{ count_pending }} - </div> -</div> - -<div class="row"> - <div class="col-12"> - <h2 class="highlight">Invitations pending response</h2> - <a href="{% url 'invitations:list' %}">Show drafted invitations</a> - - <br> - <br> - {% include 'partials/invitations/registrationinvitation_table.html' with invitations=object_list %} - </div> -</div> - -{% endblock %} diff --git a/invitations/templates/invitations/registrationinvitation_reminder_form.html b/invitations/templates/invitations/registrationinvitation_reminder_form.html index 57a876fcb250af8c7646a7375fc5e7f10688cb05..c328668dc3dc632039fbaa7ddca64a6cbb37920f 100644 --- a/invitations/templates/invitations/registrationinvitation_reminder_form.html +++ b/invitations/templates/invitations/registrationinvitation_reminder_form.html @@ -8,7 +8,6 @@ {% block breadcrumb_items %} {{block.super}} <a href="{% url 'invitations:list' %}" class="breadcrumb-item">Registration Invitations</a> - <a href="{% url 'invitations:list_pending_invitations' %}" class="breadcrumb-item">Pending response</a> <span class="breadcrumb-item">Send reminder</span> {% endblock %} @@ -17,6 +16,7 @@ <div class="row"> <div class="col-12"> <h1 class="highlight">Send reminder for Registration Invitation</h1> + {% include 'partials/invitations/registrationinvitation_summary.html' with invitation=object %} </div> </div> diff --git a/invitations/templates/partials/invitations/citationnotification_summary.html b/invitations/templates/partials/invitations/citationnotification_summary.html new file mode 100644 index 0000000000000000000000000000000000000000..9bf9479b994638c448280a61a05ec630df415f37 --- /dev/null +++ b/invitations/templates/partials/invitations/citationnotification_summary.html @@ -0,0 +1,42 @@ +<table class="registration_invitation notification"> + <tr> + <th>Name</th> + <td> + {% if notification.contributor %} + {{ notification.contributor.get_title_display }} {{ notification.contributor.user.first_name }} {{ notification.contributor.user.last_name }} + {% elif notification.invitation %} + {{ notification.invitation.first_name }} {{ notification.invitation.last_name }} + {% endif %} + </td> + </tr> + <tr> + <th>Email</th> + <td> + {% if notification.contributor %} + {{ notification.contributor.user.email }} + {% elif notification.invitation %} + {{ notification.invitation.email }} + {% endif %} + </td> + </tr> + <tr> + <th>Status</th> + <td>{{ notification.processed|yesno:'Processed,Not processed' }}</td> + </tr> + <tr> + <th>Submission</th> + <td>{{ notification.submission|default:'-' }}</td> + </tr> + <tr> + <th>Publication</th> + <td>{{ notification.publication|default:'-' }}</td> + </tr> + <tr> + <th>Created</th> + <td>{{ notification.created }} (by {{ notification.created_by.first_name }} {{ notification.created_by.last_name }})</td> + </tr> + <tr> + <th>Date Sent</th> + <td>{{ notification.date_sent|default:'<em>Not sent yet</em>' }}</td> + </tr> +</table> diff --git a/invitations/templates/partials/invitations/citationnotification_table.html b/invitations/templates/partials/invitations/citationnotification_table.html new file mode 100644 index 0000000000000000000000000000000000000000..d2e3a598eef7dc5a6ca86d547bb66e0b90cc0e54 --- /dev/null +++ b/invitations/templates/partials/invitations/citationnotification_table.html @@ -0,0 +1,49 @@ +<table class="table"> + <thead> + <tr> + <th>Name</th> + <th>Email</th> + <th>Type</th> + <th>Cited in</th> + <th>Created by</th> + <th>Date created</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for notification in notifications %} + <tr> + <td> + {% if notification.contributor %} + {{ notification.contributor.user.first_name }} {{ notification.contributor.user.last_name }} + {% elif notification.invitation %} + {{ notification.invitation.first_name }} {{ notification.invitation.last_name }} + {% endif %} + </td> + <td> + {% if notification.contributor %} + {{ notification.contributor.user.email }} + {% elif notification.invitation %} + {{ notification.invitation.email }} + {% endif %} + </td> + <td> + {% if notification.contributor %}For Contributor{% elif notification.invitation %}Registration Invitation{% else %}<span class="text-danger">Invalid</span>{% endif %} + </td> + <td> + {% if notification.publication %} + {{ notification.publication.citation }} + {% endif %} + {% if notification.submission %} + {{ notification.submission.arxiv_identifier_w_vn_nr }} + {% endif %} + </td> + <td>{{ notification.created_by.first_name }} {{ notification.created_by.last_name }}</td> + <td>{{ notification.created }}</td> + <td> + <a href="{% url 'invitations:citation_notification_process' notification.id %}">Process citation</a> + </td> + </tr> + {% endfor %} + </tbody> +</table> diff --git a/invitations/templates/partials/invitations/registrationinvitation_summary.html b/invitations/templates/partials/invitations/registrationinvitation_summary.html new file mode 100644 index 0000000000000000000000000000000000000000..4ff47a7f113268c1816d49986cccc5bf33fe0ac6 --- /dev/null +++ b/invitations/templates/partials/invitations/registrationinvitation_summary.html @@ -0,0 +1,53 @@ +<table class="registration_invitation"> + <tr> + <th>Name</th> + <td>{{ invitation.get_title_display }} {{ invitation.first_name }} {{ invitation.last_name }}</td> + </tr> + <tr> + <th>Email</th> + <td>{{ invitation.email }}</td> + </tr> + <tr> + <th>Status</th> + <td>{{ invitation.get_status_display }}</td> + </tr> + + <tr> + <th>Submission(s)</th> + <td> + <ul class="pl-3 mb-0"> + {% for citation in invitation.citation_notifications.for_submissions %} + <li>{{ citation.submission }}</li> + {% empty %} + <li><em>No submissions linked</em></li> + {% endfor %} + </ul> + </td> + </tr> + <tr> + <th>Publication(s)</th> + <td> + <ul class="pl-3 mb-0"> + {% for citation in invitation.citation_notifications.for_publications %} + <li>{{ citation.publication }}</li> + {% empty %} + <li><em>No publications linked</em></li> + {% endfor %} + </ul> + </td> + </tr> + <tr> + <th>Created</th> + <td>{{ invitation.created }}</td> + </tr> + <tr> + <th>Times sent</th> + <td><strong>{{ invitation.times_sent }}</strong> time{{ invitation.times_sent|pluralize }}{% if invitation.times_sent %} (last time: {{ invitation.date_sent_last }}){% endif %}</td> + </tr> +</table> + +{% if invitation.personal_message %} + <br> + <strong>Personal Message</strong> + <p>{{ invitation.personal_message|linebreaksbr }}</p> +{% endif %} diff --git a/invitations/templates/partials/invitations/registrationinvitation_table.html b/invitations/templates/partials/invitations/registrationinvitation_table.html index 020ce0e8103e7bf5b98c5f6c8d9f48730f647e4f..ad2056ab8cafdeb8c0be6fd1629f51c5f7be55b5 100644 --- a/invitations/templates/partials/invitations/registrationinvitation_table.html +++ b/invitations/templates/partials/invitations/registrationinvitation_table.html @@ -3,55 +3,68 @@ <table class="table"> <thead> <tr> - <th>Last name</th> - <th>First name</th> + <th>Name</th> <th>Email</th> - <th>Date</th> + <th>Status</th> <th>Type</th> <th>Drafted by</th> - <th{% if perms.scipost.can_manage_registration_invitations %} colspan="2"{% endif %}>Actions</th> + <th>Date created</th> + <th>Times sent</th> + <th colspan="2">Actions</th> </tr> </thead> <tbody> {% for invitation in invitations %} <tr> - <td>{{ invitation.last_name }}</td> - <td>{{ invitation.first_name }}</td> + <td>{{ invitation.last_name }}, {{ invitation.first_name }}</td> <td>{{ invitation.email }}</td> + <td>{{ invitation.get_status_display }}</td> + <td>{{ invitation.get_invitation_type_display }}</td> + <td>{{ invitation.created_by.first_name }} {{ invitation.created_by.last_name }}</td> + <td>{{ invitation.created }}</td> <td> - {% if invitation.status == 'draft' %} - {{ invitation.modified }} + {% if invitation.times_sent %} + <strong>{{ invitation.times_sent }}</strong> time{{ invitation.times_sent|pluralize }} + · {{ invitation.date_sent_last|timesince }} ago {% else %} - {{ invitation.date_sent_last }} + - {% endif %} </td> - <td>{{ invitation.get_invitation_type_display }}</td> - <td>{{ invitation.created_by.user.first_name }} {{ invitation.created_by.user.last_name }}</td> {% if perms.scipost.can_manage_registration_invitations %} <td> - {% if invitation.status == 'draft' %} - <a href="{% url 'invitations:update' invitation.id %}">Edit or send</a> · - <a href="{% url 'scipost:mark_draft_inv_as_processed' draft_id=invitation.id %}">Mark as send</a> - {% elif invitation.status == 'sent' %} - <a href="{% url 'invitations:send_reminder' invitation.id %}">Send reminder</a> - {% endif %} + <ul class="pl-3 mb-0"> + {% if invitation.status == 'draft' %} + <li><a href="{% url 'invitations:update' invitation.id %}">Edit or send</a></li> + <li><a href="{% url 'invitations:mark' invitation.id 'sent' %}">Mark as sent</a></li> + {% elif invitation.status == 'sent' or invitation.status == 'edited' %} + <li><a href="{% url 'invitations:send_reminder' invitation.id %}">Send reminder</a></li> + {% endif %} + </ul> + </td> + <td> + <ul class="mb-0"> + {% for ac in invitation|associated_contributors %} + <li> + <a href="{% url 'invitations:map_to_contributor' pk=invitation.id contributor_id=ac.id %}">Map to {{ ac.user.first_name }} {{ ac.user.last_name }}</a> + </li> + {% endfor %} + <li><a href="{% url 'invitations:add_citation' invitation.id %}">Add new Citation to Invitation</a></li> + </ul> + </td> + {% else %} + <td colspan="2"> + <ul class="pl-3 mb-0"> + <li><a href="{% url 'invitations:add_citation' invitation.id %}">Add new Citation to Invitation</a></li> + {% if invitation.status == 'draft' and invitation.created_by == request.user %} + <li><a href="{% url 'invitations:update' invitation.id %}">Edit Invitation</a></li> + {% endif %} + </ul> </td> {% endif %} - <td> - <ul class="mb-0"> - {% for ac in invitation|associated_contributors %} - <li> - <a href="{% url 'scipost:map_draft_reg_inv_to_contributor' draft_id=invitation.id contributor_id=ac.id %}">Map to {{ ac.user.first_name }} {{ ac.user.last_name }}</a> - </li> - {% empty %} - <li>No associated contributors found.</li> - {% endfor %} - </ul> - </td> </tr> {% empty %} <tr> - <td colspan="8">No Invitations found.</td> + <td colspan="9">No Invitations found.</td> </tr> {% endfor %} </tbody> diff --git a/invitations/urls.py b/invitations/urls.py index ee3e5a35d588fa37e60939efb57b5501a7b93319..d53b7ea3ef19fb78c221549ca4d81d78fedc8f33 100644 --- a/invitations/urls.py +++ b/invitations/urls.py @@ -4,11 +4,23 @@ from . import views urlpatterns = [ url(r'^$', views.RegistrationInvitationsView.as_view(), name='list'), - url(r'^pending_invitations$', views.PendingRegistrationInvitationsView.as_view(), - name='list_pending_invitations'), - url(r'^new$', views.RegistrationInvitationsCreateView.as_view(), name='new'), + url(r'^new$', views.create_registration_invitation_or_citation, name='new'), url(r'^(?P<pk>[0-9]+)$', views.RegistrationInvitationsUpdateView.as_view(), name='update'), - url(r'^(?P<pk>[0-9]+)/delete$', views.RegistrationInvitationsDeleteView.as_view(), name='delete'), - url(r'^(?P<pk>[0-9]+)/send_reminder$', views.RegistrationInvitationsReminderView.as_view(), name='send_reminder'), + url(r'^(?P<pk>[0-9]+)/add_citation$', views.RegistrationInvitationsAddCitationView.as_view(), + name='add_citation'), + url(r'^(?P<pk>[0-9]+)/delete$', views.RegistrationInvitationsDeleteView.as_view(), + name='delete'), + 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]+)$', + views.RegistrationInvitationsMapToContributorView.as_view(), + name='map_to_contributor'), + url(r'^(?P<pk>[0-9]+)/send_reminder$', views.RegistrationInvitationsReminderView.as_view(), + name='send_reminder'), url(r'^cleanup$', views.cleanup, name='cleanup'), + + url(r'^citations$', views.CitationNotificationsView.as_view(), + name='citation_notification_list'), + url(r'^citations/(?P<pk>[0-9]+)$', views.CitationNotificationsProcessView.as_view(), + name='citation_notification_process'), ] diff --git a/invitations/utils.py b/invitations/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4ba915726de3348db196f64b66607aff45c18fc6 --- /dev/null +++ b/invitations/utils.py @@ -0,0 +1,31 @@ +from common.utils import BaseMailUtil + + +class Utils(BaseMailUtil): + mail_sender = 'invitations@scipost.org' + mail_sender_title = 'SciPost Invitation' + + @classmethod + def invite_contributor_email(cls): + """ + Send email to unregistered people inviting them to become a SciPost Contributor. + Requires context to contain 'registration_invitation' (RegistrationInvitation instance). + """ + raise NotImplementedError('invite_contributor_email') + + @classmethod + def invite_contributor_reminder_email(cls): + """ + Send reminder(!) email to unregistered people inviting them to become a SciPost + Contributor. + Requires context to contain 'registration_invitation'(RegistrationInvitation instance). + """ + raise NotImplementedError('invite_contributor_reminder_email') + + @classmethod + def citation_notifications_email(cls): + """ + Send email to a SciPost Contributor about a Citation Notification that's been created + for him/her. Requires context to contain 'notifications' (list of CitationNotifications). + """ + raise NotImplementedError('citation_notifications_email') diff --git a/invitations/views.py b/invitations/views.py index 7dfe2214f3e428c474ec00b8ddbbbf944c02f73b..024172cfc6c6edb9b8166f7804faf3f81d2d0582 100644 --- a/invitations/views.py +++ b/invitations/views.py @@ -1,25 +1,35 @@ +from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin -from django.shortcuts import render -from django.urls import reverse_lazy +from django.db import transaction +from django.shortcuts import render, redirect +from django.urls import reverse_lazy, reverse from django.views.generic.list import ListView -from django.views.generic.edit import CreateView, UpdateView, DeleteView +from django.views.generic.edit import UpdateView, DeleteView -from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm -from .mixins import RegistrationInvitationFormMixin -from .models import RegistrationInvitation +from .forms import RegistrationInvitationForm, RegistrationInvitationReminderForm,\ + RegistrationInvitationMarkForm, RegistrationInvitationMapToContributorForm,\ + CitationNotificationForm, ContributorSearchForm, RegistrationInvitationFilterForm,\ + CitationNotificationProcessForm, RegistrationInvitationAddCitationForm +from .mixins import RequestArgumentMixin, PermissionsMixin, SaveAndSendFormMixin +from .models import RegistrationInvitation, CitationNotification from scipost.models import Contributor +from mails.mixins import MailEditorMixin -class RegistrationInvitationsView(LoginRequiredMixin, PermissionRequiredMixin, ListView): +class RegistrationInvitationsView(PermissionsMixin, ListView): permission_required = 'scipost.can_create_registration_invitations' - queryset = RegistrationInvitation.objects.drafts() + queryset = RegistrationInvitation.objects.no_response() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['count_in_draft'] = RegistrationInvitation.objects.drafts().count() - context['count_pending'] = RegistrationInvitation.objects.pending_response().count() + context['count_pending'] = RegistrationInvitation.objects.sent().count() + search_form = RegistrationInvitationFilterForm(self.request.GET or None) + if search_form.is_valid(): + context['object_list'] = search_form.search(context['object_list']) + context['object_list'] = context['object_list'].order_by('status', 'last_name') + context['search_form'] = search_form return context def get_queryset(self, *args, **kwargs): @@ -29,23 +39,78 @@ class RegistrationInvitationsView(LoginRequiredMixin, PermissionRequiredMixin, L return qs -class PendingRegistrationInvitationsView(RegistrationInvitationsView): +class CitationNotificationsView(PermissionsMixin, ListView): permission_required = 'scipost.can_manage_registration_invitations' - queryset = RegistrationInvitation.objects.pending_response() - template_name = 'invitations/registrationinvitation_pending_list.html' + queryset = CitationNotification.objects.unprocessed().prefetch_related( + 'invitation', 'contributor', 'contributor__user') -class RegistrationInvitationsCreateView(RegistrationInvitationFormMixin, CreateView): - permission_required = 'scipost.can_create_registration_invitations' - form_class = RegistrationInvitationForm - model = RegistrationInvitation - success_url = reverse_lazy('invitations:list') +class CitationNotificationsProcessView(PermissionsMixin, RequestArgumentMixin, UpdateView): + permission_required = 'scipost.can_manage_registration_invitations' + form_class = CitationNotificationProcessForm + queryset = CitationNotification.objects.unprocessed() + success_url = reverse_lazy('invitations:citation_notification_list') + + +@login_required +@permission_required('scipost.can_create_registration_invitations', raise_exception=True) +@transaction.atomic +def create_registration_invitation_or_citation(request): + """ + Create a new Registration Invitation or Citation Notification, depending whether + it is meant for an already existing Contributor or not. + """ + contributors = [] + suggested_invitations = [] + contributor_search_form = ContributorSearchForm(request.GET or None) + if contributor_search_form.is_valid(): + contributors, suggested_invitations = contributor_search_form.search() + citation_form = CitationNotificationForm(request.POST or None, contributors=contributors, + prefix='notification', request=request) + + # New citation is related to a Contributor: CitationNotification + if citation_form.is_valid(): + citation_form.save() + messages.success(request, 'New Citation Notification created') + if request.POST.get('save') == 'save_and_create': + return redirect('invitations:new') + return redirect('invitations:list') + + # New citation is related to a Contributor: RegistationInvitation + invitation_form = RegistrationInvitationForm(request.POST or None, request=request, + prefix='invitation') + if invitation_form.is_valid(): + invitation_form.save() + messages.success(request, 'New Registration Invitation created') + if request.POST.get('save') == 'save_and_create': + return redirect('invitations:new') + return redirect('invitations:list') + + context = { + 'contributor_search_form': contributor_search_form, + 'citation_form': citation_form, + 'suggested_invitations': suggested_invitations, + 'invitation_form': invitation_form, + } + return render(request, 'invitations/registrationinvitation_form_add_new.html', context) -class RegistrationInvitationsUpdateView(RegistrationInvitationFormMixin, UpdateView): +class RegistrationInvitationsUpdateView(RequestArgumentMixin, PermissionsMixin, + SaveAndSendFormMixin, MailEditorMixin, UpdateView): permission_required = 'scipost.can_create_registration_invitations' form_class = RegistrationInvitationForm - success_url = reverse_lazy('invitations:list') + utils_email_method = 'invite_contributor_email' + mail_code = 'registration_invitation' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['invitation_form'] = context['form'] + return context + + def get_success_url(self): + if self.request.POST.get('save') == 'save_and_create': + return reverse('invitations:new') + return reverse('invitations:list') def get_queryset(self, *args, **kwargs): qs = RegistrationInvitation.objects.drafts() @@ -56,14 +121,42 @@ class RegistrationInvitationsUpdateView(RegistrationInvitationFormMixin, UpdateV return qs -class RegistrationInvitationsReminderView(RegistrationInvitationFormMixin, UpdateView): +class RegistrationInvitationsAddCitationView(RequestArgumentMixin, PermissionsMixin, UpdateView): + permission_required = 'scipost.can_create_registration_invitations' + form_class = RegistrationInvitationAddCitationForm + template_name = 'invitations/registrationinvitation_form_add_citation.html' + success_url = reverse_lazy('invitations:list') + queryset = RegistrationInvitation.objects.no_response() + + +class RegistrationInvitationsMarkView(RequestArgumentMixin, PermissionsMixin, UpdateView): permission_required = 'scipost.can_manage_registration_invitations' - queryset = RegistrationInvitation.objects.pending_response() + queryset = RegistrationInvitation.objects.drafts() + form_class = RegistrationInvitationMarkForm + template_name = 'invitations/registrationinvitation_form_mark_as.html' + success_url = reverse_lazy('invitations:list') + + +class RegistrationInvitationsMapToContributorView(RequestArgumentMixin, PermissionsMixin, + UpdateView): + permission_required = 'scipost.can_manage_registration_invitations' + model = RegistrationInvitation + form_class = RegistrationInvitationMapToContributorForm + template_name = 'invitations/registrationinvitation_form_map_to_contributor.html' + success_url = reverse_lazy('invitations:list') + + +class RegistrationInvitationsReminderView(RequestArgumentMixin, PermissionsMixin, + SaveAndSendFormMixin, UpdateView): + permission_required = 'scipost.can_manage_registration_invitations' + queryset = RegistrationInvitation.objects.sent() + success_url = reverse_lazy('invitations:list') form_class = RegistrationInvitationReminderForm template_name = 'invitations/registrationinvitation_reminder_form.html' + utils_email_method = 'invite_contributor_reminder_email' -class RegistrationInvitationsDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): +class RegistrationInvitationsDeleteView(PermissionsMixin, DeleteView): permission_required = 'scipost.can_manage_registration_invitations' model = RegistrationInvitation success_url = reverse_lazy('invitations:list') @@ -77,8 +170,7 @@ def cleanup(request): database of registered Contributors. Flags overlaps. """ contributor_email_list = Contributor.objects.values_list('user__email', flat=True) - invitations = RegistrationInvitation.objects.pending_response().filter( - email__in=contributor_email_list) + invitations = RegistrationInvitation.objects.sent().filter(email__in=contributor_email_list) context = { 'invitations': invitations } diff --git a/mails/forms.py b/mails/forms.py index a34a5107cde3ba26cc27d88356ae10ecdb478637..4eee0da3e10758195dc264f19773d766af3a9773 100644 --- a/mails/forms.py +++ b/mails/forms.py @@ -20,33 +20,40 @@ class EmailTemplateForm(forms.Form): extra_recipient = forms.EmailField(label="Optional: bcc this email to", required=False) prefix = 'mail_form' - def __init__(self, *args, **kwargs): + def __init__(self, data, *args, **kwargs): self.mail_code = kwargs.pop('mail_code') self.mail_fields = None + # This is a pseudo-modelform; it does not take `instance` by default + instance = kwargs.pop('instance', None) + + # This form shouldn't be is_bound==True is there is any non-relavant POST data given. + if '%s-subject' % self.prefix in data.keys(): + data = { + '%s-subject' % self.prefix: data.get('%s-subject' % self.prefix), + '%s-text' % self.prefix: data.get('%s-text' % self.prefix), + '%s-extra_recipient' % self.prefix: data.get('%s-extra_recipient' % self.prefix), + } + else: + data = None + super().__init__(data, *args, **kwargs) + + # Gather meta data + json_location = '%s/mails/templates/mail_templates/%s.json' % (settings.BASE_DIR, + self.mail_code) + self.mail_data = json.loads(open(json_location).read()) - data = {} - if args[0]: - if args[0].get('subject'): - data['subject'] = args[0]['subject'] - if args[0].get('text'): - data['text'] = args[0]['text'] - if args[0].get('extra_recipient'): - data['extra_recipient'] = args[0]['extra_recipient'] - super().__init__(data or None) - - # Gather data + # Digest the templates mail_template = loader.get_template('mail_templates/%s.html' % self.mail_code) + if instance and self.mail_data.get('context_object'): + kwargs[self.mail_data['context_object']] = instance mail_template = mail_template.render(kwargs) # self.doc = html.fromstring(mail_template) # self.doc2 = self.doc.text_content() # print(self.doc2) - json_location = '%s/mails/templates/mail_templates/%s.json' % (settings.BASE_DIR, - self.mail_code) - self.mail_data = json.loads(open(json_location).read()) - # Object self.object = kwargs.get(self.mail_data.get('context_object', ''), None) + self.object = self.object or instance self.recipient = None if self.object: recipient = self.object @@ -141,6 +148,8 @@ class EmailTemplateForm(forms.Form): reply_to=[self.mail_data.get('from_address', 'no-reply@scipost.org')]) email.attach_alternative(self.mail_fields['html_message'], 'text/html') email.send(fail_silently=False) + if self.object and hasattr(self.object, 'mail_sent'): + self.object.mail_sent() class HiddenDataForm(forms.Form): diff --git a/mails/mixins.py b/mails/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..4d07529c8b5fd0e5c2bf4f24a9b418b5b7a6f6f1 --- /dev/null +++ b/mails/mixins.py @@ -0,0 +1,75 @@ +from .forms import EmailTemplateForm, HiddenDataForm + + +class MailEditorMixin: + """ + Use MailEditorMixin in edit CBVs to automatically implement the mail editor as + a post-form_valid hook. + + The view must specify the `mail_code` variable. + """ + object = None + mail_form = None + + def __init__(self, *args, **kwargs): + if not self.mail_code: + raise AttributeError(self.__class__.__name__ + ' object has no attribute `mail_code`') + super().__init__(*args, **kwargs) + + def get_template_names(self): + """ + The mail editor form has its own template. + """ + if self.mail_form and not self.mail_form.is_valid(): + return ['mails/mail_form.html'] + return super().get_template_names() + + def post(self, request, *args, **kwargs): + """ + Handle POST requests, but interpect the data if the mail form data isn't valid. + """ + self.object = self.get_object() + form = self.get_form() + if form.is_valid(): + self.mail_form = EmailTemplateForm(request.POST or None, + mail_code=self.mail_code, + instance=self.object) + if self.mail_form.is_valid(): + return self.form_valid(form) + + return self.render_to_response( + self.get_context_data(form=self.mail_form, + transfer_data_form=HiddenDataForm(form))) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """ + If both the regular form and mailing form are valid, save the form and run the mail form. + """ + self.mail_form.send() + return super().form_valid(form) + + + # def __init__(self, request, mail_code, **kwargs): + # self.request = request + # self.context = kwargs.get('context', {}) + # self.template_name = kwargs.get('template', 'mails/mail_form.html') + # self.mail_form = EmailTemplateForm(request.POST or None, mail_code=mail_code, **kwargs) + # + # @property + # def recipients_string(self): + # return ', '.join(getattr(self.mail_form, 'mail_fields', {}).get('recipients', [''])) + # + # def add_form(self, form): + # self.context['transfer_data_form'] = HiddenDataForm(form) + # + # def is_valid(self): + # return self.mail_form.is_valid() + # + # def send(self): + # return self.mail_form.send() + # + # def return_render(self): + # self.context['form'] = self.mail_form + # return render(self.request, self.template_name, self.context) diff --git a/mails/templates/mail_templates/registration_invitation.html b/mails/templates/mail_templates/registration_invitation.html index 0202042a577c7c213ca9280752a95e17febd011b..4990434fcb7ff6172e336db008f2343eddff1b26 100644 --- a/mails/templates/mail_templates/registration_invitation.html +++ b/mails/templates/mail_templates/registration_invitation.html @@ -1,11 +1,3 @@ -{% if renew %} - Reminder: Invitation to SciPost - ------------------------------- - - <strong>Reminder: Invitation to SciPost</strong> - <br/> -{% endif %} - {% if invitation.invitation_type == 'F' %} <strong>RE: Invitation to join the Editorial College of SciPost</strong> <br> diff --git a/mails/templates/mail_templates/registration_invitation.json b/mails/templates/mail_templates/registration_invitation.json index 9941b614623cf8f70ca83bcfbf4f934307d93b3f..639804acfbc0fb392c5675b52a7a53fb42106189 100644 --- a/mails/templates/mail_templates/registration_invitation.json +++ b/mails/templates/mail_templates/registration_invitation.json @@ -1,7 +1,7 @@ { "subject": "SciPost: invitation", "to_address": "email", - "bcc_to": "invited_by.user.email", + "bcc_to": "jscaux@scipost.org", "from_address_name": "J-S Caux", "from_address": "jscaux@scipost.org", "context_object": "invitation" diff --git a/mails/templates/mails/mail_form.html b/mails/templates/mails/mail_form.html index 651e840fe09cc5808caf604e33a0788b35faa701..4b4f9dd316e2268e89e46af6bad12d20ea79ded3 100644 --- a/mails/templates/mails/mail_form.html +++ b/mails/templates/mails/mail_form.html @@ -9,8 +9,6 @@ <h1>Complete and send mail</h1> <h3 class="mb-4">You may edit the mail before sending it.</h3> - {{ transfer_data }} - <form enctype="multipart/form-data" method="post"> {% csrf_token %} {% if transfer_data_form %}{{ transfer_data_form }}{% endif %} diff --git a/scipost/migrations/0005_auto_20180218_1556.py b/scipost/migrations/0005_auto_20180218_1556.py new file mode 100644 index 0000000000000000000000000000000000000000..5ac3f68be6966a91551ed637d0e0bd7f91bce982 --- /dev/null +++ b/scipost/migrations/0005_auto_20180218_1556.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-02-18 14:56 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('scipost', '0004_auto_20180212_1932'), + ] + + operations = [ + migrations.AlterField( + model_name='registrationinvitation', + name='first_name', + field=models.CharField(max_length=30), + ), + migrations.AlterField( + model_name='registrationinvitation', + name='last_name', + field=models.CharField(max_length=30), + ), + ] diff --git a/scipost/static/scipost/assets/css/_buttons.scss b/scipost/static/scipost/assets/css/_buttons.scss index 6abdc7c02b81ac4c4a9c4d8ce9cc82c87f0ef977..7a7b57a4ec67b3e60f7432b80d8e0bebc8b3362d 100644 --- a/scipost/static/scipost/assets/css/_buttons.scss +++ b/scipost/static/scipost/assets/css/_buttons.scss @@ -12,6 +12,10 @@ } } +.btn-link { + color: $scipost-lightblue; +} + .btn-secondary { color: $scipost-darkblue; background-color: $white; diff --git a/scipost/static/scipost/assets/css/_form.scss b/scipost/static/scipost/assets/css/_form.scss index e0182d0420dcb8d74eccf2521dc7e72103560f7e..27a45b99ee238df2992b68646487ce2b4e30a9ec 100644 --- a/scipost/static/scipost/assets/css/_form.scss +++ b/scipost/static/scipost/assets/css/_form.scss @@ -117,3 +117,24 @@ select.form-control { height: auto; } } + + +// Autocomplete fields +.results_on_deck { + .help-block { + color: #666; + font-style: italic; + } + + > div { + background-color: $scipost-lightestblue; + padding: 2px 10px; + display: inline-block; + border-radius: 5px; + + .ui-icon { + margin-top: 0; + margin-right: 3px; + } + } +} diff --git a/scipost/static/scipost/assets/css/_tables.scss b/scipost/static/scipost/assets/css/_tables.scss index 8873b93c394e15c1ef958f4509818a1f193e5c59..7c3d0fdc6c1d930fd04287fd8b90807a6e4410b8 100644 --- a/scipost/static/scipost/assets/css/_tables.scss +++ b/scipost/static/scipost/assets/css/_tables.scss @@ -12,6 +12,8 @@ } table.commentary td, +table.registration_invitation td, +table.registration_invitation th, table.submission td { padding: 0.1em 0.7em;